Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ builds:
- main: ./cmd/fullsend/
binary: fullsend
ldflags:
- -s -w -X github.com/fullsend-ai/fullsend/internal/cli.version={{.Version}}
- -s -w -X github.com/fullsend-ai/fullsend/internal/cli.version={{.Version}} -X github.com/fullsend-ai/fullsend/internal/cli.commitSHA={{.FullCommit}}
env:
- CGO_ENABLED=0
goos:
Expand Down
2 changes: 1 addition & 1 deletion docs/ADRs/0006-ordered-layer-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Installing fullsend into an org involves multiple concerns with ordering depende

Each installation concern is a `Layer` implementing `Install`, `Uninstall`, and `Analyze`. Layers are composed into an ordered `Stack`. Install runs layers forward; uninstall runs them in reverse; analyze runs them forward and collects reports.

The current stack order is: config-repo → workflows → secrets → inference → dispatch-token → enrollment.
The current stack order is: config-repo → workflows → harness-wrappers → vendor-binary → secrets → inference → dispatch-token → enrollment.

Each layer is idempotent — re-running install skips already-completed work. Uninstall collects all errors rather than stopping on the first, so partial teardown still makes progress. Each layer declares the OAuth scopes it needs via `RequiredScopes`, enabling a preflight check that fails early when the token lacks required permissions.

Expand Down
15 changes: 15 additions & 0 deletions docs/ADRs/0045-forge-portable-harness-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,21 @@ forge-specific artifact. The harness and agent definition are portable.
removing an agent is deleting a file, adding one is creating a thin
wrapper with `base:`.

- **Default URL allowlist for `base` composition.** `fullsend install`
sets `allowed_remote_resources` in `config.yaml` to include the
fullsend scaffold URL prefix
(`https://raw.githubusercontent.com/fullsend-ai/fullsend/`), ensuring
generated `base:` URLs pass the allowlist without manual configuration.
Integrity is enforced by the mandatory `#sha256=...` hash in each URL.

- **Phase 2 dual-write.** During Phase 2, agent identity (`role`, `slug`)
is written to both `config.yaml`'s `agents:` block and harness wrapper
files. The `agents:` block remains the source of truth for existing
consumers (`loadKnownSlugs`, `runUninstall`, `SecretsLayer`). Phase 3
migrates consumers to harness-file discovery; Phase 4 removes the
`agents:` block. Reconciliation between the two is not needed because
both are written atomically during `fullsend install`.

- **Merge semantics add complexity.** The inheritance rules (scalars
override, skills concatenate, runner_env merges, validation_loop replaces)
must be well-documented and tested. Edge cases — such as a forge block
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Infrastructure platform choice and configuration are specified in the adopting o
**Decided:**

- Forge abstraction: all forge operations go through the `forge.Client` interface, keeping the rest of the codebase forge-agnostic ([ADR 0005](ADRs/0005-forge-abstraction-layer.md)).
- Installation model: ordered layer stack (install forward, uninstall reverse, analyze for status reporting) with idempotent operations. Current stack: config-repo → workflows → secrets → inference → dispatch → enrollment ([ADR 0006](ADRs/0006-ordered-layer-model.md)).
- Installation model: ordered layer stack (install forward, uninstall reverse, analyze for status reporting) with idempotent operations. Current stack: config-repo → workflows → harness-wrappers → vendor-binary → secrets → inference → dispatch → enrollment ([ADR 0006](ADRs/0006-ordered-layer-model.md)).
- Cross-repo dispatch: enrolled repos call `.fullsend` via `workflow_call`; a dispatch workflow mints OIDC tokens exchanged at a central token mint (GCP Cloud Function) for scoped GitHub App installation tokens per agent role. App PEM secrets are stored in Secret Manager, not the config repo ([ADR 0008](ADRs/0008-workflow-dispatch-for-cross-repo-dispatch.md)).
- Shim workflow security: `pull_request_target` prevents PR authors from modifying the shim workflow. No long-lived secrets flow through the shim — OIDC tokens are issued by the GitHub runtime and scoped to the workflow run ([ADR 0009](ADRs/0009-pull-request-target-in-shim-workflows.md)).
- Repo maintenance: a workflow in `.fullsend` (`.github/workflows/repo-maintenance.yml`) reconciles enrollment shims in target repos when `config.yaml` changes or on manual dispatch. The CLI's `EnrollmentLayer.Install()` dispatches this workflow via `workflow_dispatch` and monitors it for completion, then reports any enrollment PRs created in target repos.
Expand Down
6 changes: 3 additions & 3 deletions docs/guides/dev/cli-internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,9 @@ type Layer interface {
```

```
Stack order: ConfigRepo → Workflows → VendorBinary → Secrets → Inference → Dispatch → Enrollment
Install: process 1→7 (forward)
Uninstall: process 7→1 (reverse)
Stack order: ConfigRepo → Workflows → HarnessWrappers → VendorBinary → Secrets → Inference → Dispatch → Enrollment
Install: process 1→8 (forward)
Uninstall: process 8→1 (reverse)
```

Per-repo mode does not use the layer stack — it runs the same phases inline in `runPerRepoInstall()` and `runGitHubSetupPerRepo()` since there's no need for composable uninstall ordering with a single repo. Binary vendoring (when `--vendor-fullsend-binary` is set) and stale binary cleanup are handled inline or via shared helpers; per-org mode uses `VendorBinaryLayer`.
Expand Down
14 changes: 8 additions & 6 deletions docs/plans/vertex-inference-provisioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,12 @@ type InferenceLayer struct {

1. ConfigRepoLayer
2. WorkflowsLayer
3. SecretsLayer (agent app keys)
4. **InferenceLayer** (inference provider credentials) ← NEW
5. DispatchTokenLayer
6. EnrollmentLayer
3. HarnessWrappersLayer
4. VendorBinaryLayer
5. SecretsLayer (agent app keys)
6. **InferenceLayer** (inference provider credentials) ← NEW
7. DispatchTokenLayer
8. EnrollmentLayer

Rationale: InferenceLayer needs `.fullsend` repo to exist (created by ConfigRepoLayer) and stores repo-level secrets (like SecretsLayer). It must run before EnrollmentLayer since enrolled repos will need these secrets available.

Expand Down Expand Up @@ -228,9 +230,9 @@ Add the InferenceLayer to the stack between SecretsLayer and DispatchTokenLayer.

#### 5a. `docs/ADRs/0006-ordered-layer-model.md` — Update layer stack ordering

This is the canonical ADR defining the layer model. The Consequences section lists the current stack as `config-repo → workflows → secrets → dispatch-token → enrollment`. Update to include InferenceLayer at position 4:
This is the canonical ADR defining the layer model. Update to include InferenceLayer at position 4:

`config-repo → workflows → secrets → inference → dispatch-token → enrollment`
`config-repo → workflows → harness-wrappers → vendor-binary → secrets → inference → dispatch-token → enrollment`

#### 5b. `docs/architecture.md` — Update architecture overview

Expand Down
8 changes: 5 additions & 3 deletions internal/cli/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -1211,7 +1211,7 @@ func runDryRun(ctx context.Context, client forge.Client, printer *ui.Printer, or
} else {
dispatcher = gcf.NewProvisioner(gcf.Config{}, nil)
}
stack := buildLayerStack(org, client, cfg, printer, user, privateRepo, enabledRepos, agentCreds, enrolledRepoIDs, inferenceProvider, vendorBinary, makeVendorFunc(fullsendBinary), dispatcher)
stack := buildLayerStack(org, client, cfg, printer, user, privateRepo, enabledRepos, agentCreds, enrolledRepoIDs, inferenceProvider, vendorBinary, makeVendorFunc(fullsendBinary), dispatcher, commitSHA)

if err := runPreflight(ctx, stack, layers.OpInstall, client, printer); err != nil {
return err
Expand Down Expand Up @@ -1572,7 +1572,7 @@ func runInstall(ctx context.Context, client forge.Client, printer *ui.Printer, o
}, gcf.NewLiveGCFClient(mintProject))
}

stack := buildLayerStack(org, client, cfg, printer, user, privateRepo, enabledRepos, agentCreds, enrolledRepoIDs, inferenceProvider, vendorBinary, makeVendorFunc(fullsendBinary), disp)
stack := buildLayerStack(org, client, cfg, printer, user, privateRepo, enabledRepos, agentCreds, enrolledRepoIDs, inferenceProvider, vendorBinary, makeVendorFunc(fullsendBinary), disp, commitSHA)

if err := runPreflight(ctx, stack, layers.OpInstall, client, printer); err != nil {
return err
Expand Down Expand Up @@ -1818,7 +1818,7 @@ func runAnalyze(ctx context.Context, client forge.Client, printer *ui.Printer, o
}

dispatcher := gcf.NewProvisioner(gcf.Config{}, nil)
stack := buildLayerStack(org, client, cfg, printer, user, privateRepo, nil, agentCreds, nil, inferenceProvider, false, nil, dispatcher)
stack := buildLayerStack(org, client, cfg, printer, user, privateRepo, nil, agentCreds, nil, inferenceProvider, false, nil, dispatcher, commitSHA)

if err := runPreflight(ctx, stack, layers.OpAnalyze, client, printer); err != nil {
return err
Expand All @@ -1843,6 +1843,7 @@ func buildLayerStack(
vendorBinary bool,
Comment thread
ggallen marked this conversation as resolved.
vendorFn layers.VendorFunc,
dispatcher dispatch.Dispatcher,
commitSHA string,
) *layers.Stack {
dispatchLayer := layers.NewOIDCDispatchLayer(org, client, enrolledRepoIDs, dispatcher, printer)

Expand All @@ -1859,6 +1860,7 @@ func buildLayerStack(
return layers.NewStack(
layers.NewConfigRepoLayer(org, client, cfg, printer, privateRepo),
layers.NewWorkflowsLayer(org, client, printer, user, version),
layers.NewHarnessWrappersLayer(org, client, printer, agentCreds, commitSHA),
layers.NewVendorBinaryLayer(org, forge.ConfigRepoName, client, printer, vendorBinary, vendorFn),
layers.NewSecretsLayer(org, client, agentCreds, printer).WithOIDCMode(),
layers.NewInferenceLayer(org, client, inferenceProvider, printer),
Expand Down
3 changes: 3 additions & 0 deletions internal/cli/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,7 @@ func TestBuildLayerStack_NilEnabledRepos_SkipsDisabledRepos(t *testing.T) {
false, // vendorBinary
nil, // vendorFn
nil, // dispatcher
"dev", // commitSHA
)

// The enrollment layer (last in the stack) should have no repos to
Expand Down Expand Up @@ -1134,6 +1135,7 @@ func TestBuildLayerStack_EmptyEnabledRepos_IncludesDisabledRepos(t *testing.T) {
false,
[]string{}, // explicitly empty (not nil)
nil, nil, nil, false, nil, nil,
"dev", // commitSHA
)

// The enrollment layer should have disabled repos to reconcile.
Expand Down Expand Up @@ -1211,6 +1213,7 @@ func TestCheckInstallScopes_SyncWithLayers(t *testing.T) {
stack := layers.NewStack(
layers.NewConfigRepoLayer("test-org", nil, emptyCfg, ui.New(&discardWriter{}), false),
layers.NewWorkflowsLayer("test-org", nil, ui.New(&discardWriter{}), "", "test-version"),
layers.NewHarnessWrappersLayer("test-org", nil, ui.New(&discardWriter{}), nil, "dev"),
layers.NewSecretsLayer("test-org", nil, nil, ui.New(&discardWriter{})),
layers.NewInferenceLayer("test-org", nil, nil, ui.New(&discardWriter{})),
layers.NewOIDCDispatchLayer("test-org", nil, nil, nil, ui.New(&discardWriter{})),
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ func runGitHubSetupPerOrg(ctx context.Context, client forge.Client, printer *ui.
vendorFn = makeVendorFunc(cfg.fullsendBinary)
}

stack := buildLayerStack(org, client, orgCfg, printer, user, privateRepo, enabledRepos, agentCreds, enrolledRepoIDs, inferenceProvider, cfg.vendorBinary, vendorFn, dispatcher)
stack := buildLayerStack(org, client, orgCfg, printer, user, privateRepo, enabledRepos, agentCreds, enrolledRepoIDs, inferenceProvider, cfg.vendorBinary, vendorFn, dispatcher, commitSHA)

if cfg.dryRun {
printer.Header("Dry run — analyzing what setup would do")
Expand Down Expand Up @@ -486,7 +486,7 @@ func runGitHubSetupPerOrg(ctx context.Context, client forge.Client, printer *ui.
orgCfg = config.NewOrgConfig(repoNames, enabledRepos, roles, agents, inferenceProviderName)
orgCfg.Dispatch.Mode = "oidc-mint"

stack = buildLayerStack(org, client, orgCfg, printer, user, privateRepo, enabledRepos, agentCreds, enrolledRepoIDs, inferenceProvider, cfg.vendorBinary, vendorFn, dispatcher)
stack = buildLayerStack(org, client, orgCfg, printer, user, privateRepo, enabledRepos, agentCreds, enrolledRepoIDs, inferenceProvider, cfg.vendorBinary, vendorFn, dispatcher, commitSHA)
}

if err := runPreflight(ctx, stack, layers.OpInstall, client, printer); err != nil {
Expand Down
6 changes: 6 additions & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ import (
)

var version = "dev"
var commitSHA = "dev"
Comment thread
ggallen marked this conversation as resolved.

// Version returns the CLI version string set at build time.
func Version() string {
return version
}

// CommitSHA returns the git commit SHA set at build time.
func CommitSHA() string {
return commitSHA
}

func newRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "fullsend",
Expand Down
4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ func NewOrgConfig(allRepos, enabledRepos, roles []string, agents []AgentEntry, i
},
Agents: agents,
Repos: repos,
Comment thread
ggallen marked this conversation as resolved.
Comment thread
ggallen marked this conversation as resolved.
Comment thread
ggallen marked this conversation as resolved.
Comment thread
ggallen marked this conversation as resolved.
// Default allowlist for base: composition in harness wrappers (ADR-0045 Phase 2).
AllowedRemoteResources: []string{
Comment thread
ggallen marked this conversation as resolved.
"https://raw.githubusercontent.com/fullsend-ai/fullsend/",
},
}
if inferenceProvider != "" {
cfg.Inference = InferenceConfig{Provider: inferenceProvider}
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ func TestNewOrgConfig(t *testing.T) {
assert.Equal(t, "fullsend", cfg.Agents[0].Role)
assert.Equal(t, "test", cfg.Agents[0].Name)
assert.Equal(t, "test-slug", cfg.Agents[0].Slug)

assert.Equal(t, []string{"https://raw.githubusercontent.com/fullsend-ai/fullsend/"}, cfg.AllowedRemoteResources)
}

func TestOrgConfigMarshal(t *testing.T) {
Expand Down
Loading
Loading