Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@ jobs:
run: make test-unit

- name: Run architecture tests
# CI runs arch tests against the committed VENDORED rules facts
# (tests/arch/vendored/*). It never mounts the internal jentic-one-rules
# repo — the vendored-facts OSS-safety guard runs here regardless.
run: make test-arch

test-integration:
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,9 @@ cli/AGENTS.md

# Local toolchain (Go install for CLI builds)
.toolchain/

# Private rules clone (jentic-one-rules) fetched by scripts/rules-clone.sh. Read
# at runtime (auto-detected here, or via JENTIC_RULES_DIR); its full rule prose +
# facts must NEVER be committed into this public repo. Contributors without access
# simply fall back to the vendored subset under tests/arch.
.rules/
33 changes: 32 additions & 1 deletion .harness/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ set -euo pipefail
# HARNESS_ENV_FILE is a sourced shell file the harness uses to propagate
# environment variables from this script back to subsequent harness steps.
# We don't write to it yet, but we validate it up front so future setup logic
# can append `KEY=value` lines without surprise failures.
# can append KEY=value lines without surprise failures.
: "${HARNESS_ENV_FILE:?HARNESS_ENV_FILE must be set by the harness}"
if [[ ! -w "${HARNESS_ENV_FILE}" ]]; then
echo "ERROR: HARNESS_ENV_FILE=${HARNESS_ENV_FILE} is not writable" >&2
Expand All @@ -25,4 +25,35 @@ if [[ "${1:-}" == "--teardown" ]]; then
exit 0
fi

# Fetch the internal rules repo (read-only) so harness agents read the full rule
# guidance. The harness runlet lacks the ssh binary. Instead, the harness
# injects GITHUB_APP_TOKEN_FILE with a path to a refreshable token we can use.
# --teardown: harness owns compose lifecycle; nothing to do here.
echo "[harness] Fetching internal rules repo (jentic-one-rules)..."
RULES_DIR="$(pwd)/.rules"

if [[ ! -d "${RULES_DIR}" ]]; then
# Grab the token from the harness's secure sidecar file
TOKEN=""
if [[ -n "${GITHUB_APP_TOKEN_FILE:-}" && -s "${GITHUB_APP_TOKEN_FILE}" ]]; then
TOKEN=$(cat "${GITHUB_APP_TOKEN_FILE}")
fi

if [[ -n "${TOKEN}" ]]; then
# Use --quiet so we don't accidentally leak the token URL in trace logs
if git clone --quiet "https://x-access-token:${TOKEN}@github.com/jentic/jentic-one-rules.git" "${RULES_DIR}"; then
echo "[harness] rules-clone: Successfully cloned jentic-one-rules."
else
echo "[harness] rules-clone: failed to clone (expected for OSS users without access)."
fi
else
echo "[harness] rules-clone: No GITHUB_APP_TOKEN_FILE found. Skipping rules repo clone."
fi
fi

# Export the mount so subsequent harness steps inherit it
if [[ -d "${RULES_DIR}" ]]; then
echo "JENTIC_RULES_DIR=${RULES_DIR}" >>"${HARNESS_ENV_FILE}"
fi

echo "ready"
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# AGENTS.md

> **If `.rules/` exists, read `.rules/AGENTS.md` and use it instead of this file.**
> Otherwise use the guidance below.

## In-repo guidance

This repo's agent guidance lives in **[`CLAUDE.md`](CLAUDE.md)**. Read it first.

It covers the quick start, commands, project layout (backend + `ui/` frontend),
Expand All @@ -19,3 +24,4 @@ Key conventions, in one breath:
(`alwaysApply`).

When `CLAUDE.md` and this file disagree, `CLAUDE.md` wins.

3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# CLAUDE.md

> **If `.rules/` exists, read `.rules/AGENTS.md` and use it instead of this file.**
> Otherwise use the guidance below.

## Quick Start

```bash
Expand Down
5 changes: 5 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# GEMINI.md

> **If `.rules/` exists, read `.rules/AGENTS.md` and use it instead of this file.**

Otherwise, read [`AGENTS.md`](AGENTS.md) — this repo's canonical agent guidance.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@ Tests are split into tiers:

Commits follow [Conventional Commits](https://www.conventionalcommits.org/) with a mandatory scope, enforced repo-wide by a `commit-msg` hook.

### Full development rules (jentic maintainers)

The architecture tests self-enforce against a small vendored subset of facts
([`tests/arch/vendored/`](tests/arch/vendored/)), so a plain clone needs nothing
extra. jentic maintainers with access to the internal `jentic-one-rules` repo can
pull the **full** rule guidance for local dev / agents:

```bash
scripts/rules-clone.sh # read-only clone into .rules/ (gitignored, auto-detected)
```

Once present, the arch tests and coding agents pick up the live rules
automatically — no env var needed. If you lack access the script fails soft and
the vendored subset is used, so external contributors are unaffected.

## Security & telemetry

- **Credentials stay local.** Stored credentials are encrypted at rest and are only ever decrypted inside the Broker at execution time. They are never returned to callers, logged in cleartext, or exposed to the agent.
Expand Down
15 changes: 5 additions & 10 deletions cli/internal/cmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,22 @@ package cmd

import (
"io"
"os"

"github.com/jentic/jentic-one/cli/internal/config"
)

// App is the dependency container threaded into every command constructor. It
// holds the resolved filesystem paths and the output streams, so commands carry
// no package-global state and are constructible (and testable) in isolation.
//
// App is the internal wiring derived from the exported core.AppContainer
// (see root.go's appFromContainer): the container carries the injectable seams
// a downstream package can override, while App carries the resolved paths every
// subcommand needs.
type App struct {
// Paths resolves every filesystem location the CLI owns.
Paths config.Paths
// Out and Err are the standard output streams (overridable in tests).
Out io.Writer
Err io.Writer
}

// newApp builds the default application wiring (real paths, os streams).
func newApp() (*App, error) {
paths, err := config.NewPaths()
if err != nil {
return nil, err
}
return &App{Paths: paths, Out: os.Stdout, Err: os.Stderr}, nil
}
54 changes: 46 additions & 8 deletions cli/internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (
"syscall"

"github.com/spf13/cobra"

"github.com/jentic/jentic-one/cli/internal/config"
"github.com/jentic/jentic-one/cli/pkg/core"
)

// Build-time version metadata. These are overridden via -ldflags
Expand Down Expand Up @@ -141,14 +144,49 @@ func ExecuteAPI() {
os.Exit(runRoot(newAPIRootCmd))
}

// runRoot builds the root command via build, wires a signal-cancelled context,
// and executes it. The real work lives here (rather than in Execute*) so that
// deferred cleanup (the signal-context cancel) always runs before the process
// exits.
func runRoot(build func(*App) *cobra.Command) int {
app, err := newApp()
// defaultContainer builds the default injection container (no extra commands).
// A downstream package builds its own core.AppContainer{ExtraCommands: ...} and
// calls core.NewRootCmd directly from its own main.go.
func defaultContainer() *core.AppContainer {
return &core.AppContainer{Out: os.Stdout, Err: os.Stderr}
}

// appFromContainer derives the internal App (resolved paths + streams) from the
// injected container. Paths are resolved here — the exported core package stays
// free of the internal config package, keeping the dependency edge
// internal/cmd → pkg/core one-directional.
func appFromContainer(deps *core.AppContainer) (*App, error) {
paths, err := config.NewPaths()
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
return nil, err
}
return &App{Paths: paths, Out: deps.Out, Err: deps.Err}, nil
}

// runRoot builds the root command via the built-in tree builder, wires a
// signal-cancelled context, and executes it. It composes the tree through
// core.NewRootCmd so any injected ExtraCommands are appended after the built-in
// set. The real work lives here (rather than in Execute*) so that deferred
// cleanup (the signal-context cancel) always runs before the process exits.
func runRoot(build func(*App) *cobra.Command) int {
deps := defaultContainer()

// Adapt the internal (*App)-based tree builder to core.TreeBuilder. App
// construction (path resolution) can fail; surface it as a build-time panic
// captured below rather than threading an error through the cobra tree.
var buildErr error
tree := func(d *core.AppContainer) *cobra.Command {
app, err := appFromContainer(d)
if err != nil {
buildErr = err
return &cobra.Command{RunE: func(*cobra.Command, []string) error { return err }}
}
return build(app)
}

root := core.NewRootCmd(deps, tree)
if buildErr != nil {
fmt.Fprintln(os.Stderr, "error:", buildErr)
return 1
}

Expand All @@ -157,7 +195,7 @@ func runRoot(build func(*App) *cobra.Command) int {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

err = build(app).ExecuteContext(ctx)
err := root.ExecuteContext(ctx)
if err == nil {
return 0
}
Expand Down
52 changes: 52 additions & 0 deletions cli/pkg/core/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Package core exposes the CLI's injectable dependency container and root
// builder so a downstream package can compose its own binaries without editing
// the built-in command tree. Everything here is importable (unlike internal/).
//
// Migration ordering is deliberately NOT modelled here: the Python runner
// (`python -m jentic_one.migrations.run`) owns the target set and its
// upgrade/rollback order via its DB_TARGETS registry. The CLI only invokes that
// runner (with a direction flag); it never maintains its own target list.
package core

import (
"io"

"github.com/spf13/cobra"
)

// CommandFactory builds a cobra command from the injected container. Extra
// command groups are supplied as factories so they are constructed against the
// same container (paths, streams) the built-in tree uses.
type CommandFactory func(deps *AppContainer) *cobra.Command

// TreeBuilder builds the fully-configured root command for a binary from the
// injected container. internal/cmd supplies this so `core` never imports
// `internal/*` — which keeps the dependency edge one-directional
// (internal/cmd → pkg/core) and avoids an import cycle.
type TreeBuilder func(deps *AppContainer) *cobra.Command

// AppContainer is the injected dependency set for the CLI command tree. The
// default binaries build a plain container; a downstream package builds its own
// and adds commands via ExtraCommands.
//
// It deliberately carries NO migration-target list (see the package doc).
type AppContainer struct {
// Out and Err are the standard output streams (overridable in tests).
Out io.Writer
Err io.Writer

// ExtraCommands are extra command groups appended after the built-in tree.
// nil for the default binaries.
ExtraCommands []CommandFactory
}

// NewRootCmd builds a root command tree using the injected container. `build`
// assembles the built-in command set (supplied by internal/cmd); any
// ExtraCommands are appended last so they never shadow built-in commands.
func NewRootCmd(deps *AppContainer, build TreeBuilder) *cobra.Command {
root := build(deps)
for _, f := range deps.ExtraCommands {
root.AddCommand(f(deps))
}
return root
}
45 changes: 45 additions & 0 deletions cli/pkg/core/container_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package core

import (
"io"
"testing"

"github.com/spf13/cobra"
)

// buildStub returns a minimal built-in-style tree builder for tests.
func buildStub(_ *AppContainer) *cobra.Command {
root := &cobra.Command{Use: "jentic"}
root.AddCommand(&cobra.Command{Use: "register"})
return root
}

func TestNewRootCmdBuildsTree(t *testing.T) {
deps := &AppContainer{Out: io.Discard, Err: io.Discard}
root := NewRootCmd(deps, buildStub)

if root.Use != "jentic" {
t.Fatalf("root.Use = %q, want %q", root.Use, "jentic")
}
if _, _, err := root.Find([]string{"register"}); err != nil {
t.Fatalf("expected built-in 'register' command to be present: %v", err)
}
}

func TestNewRootCmdAppendsExtraCommands(t *testing.T) {
deps := &AppContainer{
Out: io.Discard,
Err: io.Discard,
ExtraCommands: []CommandFactory{
func(_ *AppContainer) *cobra.Command { return &cobra.Command{Use: "proxy"} },
func(_ *AppContainer) *cobra.Command { return &cobra.Command{Use: "trust"} },
},
}
root := NewRootCmd(deps, buildStub)

for _, name := range []string{"register", "proxy", "trust"} {
if _, _, err := root.Find([]string{name}); err != nil {
t.Errorf("expected command %q to be present: %v", name, err)
}
}
}
20 changes: 20 additions & 0 deletions docker/local-setup/init-schemas.sql
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,23 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA control GRANT ALL ON TYPES TO control_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA admin GRANT ALL ON TABLES TO admin_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA admin GRANT ALL ON SEQUENCES TO admin_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA admin GRANT ALL ON TYPES TO admin_user;

-- ── Adding an extra schema that references these tables ─────────────────────
-- A downstream deployment can add its own isolated schema + role alongside the
-- ones above. If that schema's tables declare foreign keys into these tables,
-- the role needs USAGE on the referenced schema and REFERENCES on the target
-- tables — without them the FK creation fails with a permission error. The base
-- local-dev setup does not create any such schema (so the volume never needs
-- recreating for the default setup); wire the grants into that deployment's own
-- init script / DB-setup chart. Example shape:
--
-- CREATE SCHEMA IF NOT EXISTS extra;
-- CREATE ROLE extra_user LOGIN PASSWORD 'extra_pass';
-- GRANT USAGE, CREATE ON SCHEMA extra TO extra_user;
-- -- Grant on each referenced schema the extra tables FK into:
-- GRANT USAGE ON SCHEMA registry TO extra_user;
-- GRANT REFERENCES ON ALL TABLES IN SCHEMA registry TO extra_user;
-- ALTER DEFAULT PRIVILEGES IN SCHEMA registry
-- GRANT REFERENCES ON TABLES TO extra_user;
-- -- (repeat the three grants above for `control` / `admin` as needed)

Loading