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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion internal/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,19 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep
printer.StepDone(fmt.Sprintf("Agent-input files copied (%.1fs)", time.Since(inputStart).Seconds()))
}

// 8c. Host-side scan (Path A): scan the target repo's context files
// 8c. Make the target repo read-only if the harness opts in.
// Runs after all repo-directory writes (8a, 8a-2) are complete.
Comment thread
ben-alkov marked this conversation as resolved.
// Excludes .git/ so git operations (index.lock, etc.) still work.
if h.ReadonlyRepo {
chmodCmd := fmt.Sprintf("chmod -R a-w %s && chmod -R u+w %s/.git", remoteRepositoryDir, remoteRepositoryDir)
if _, _, _, err := sandbox.Exec(sandboxName, chmodCmd, 30*time.Second); err != nil {
printer.StepWarn("Could not make repo read-only: " + err.Error())
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
} else {
printer.StepDone("Target repo set to read-only")
}
}

// 8d. Host-side scan (Path A): scan the target repo's context files
// (CLAUDE.md, AGENTS.md, SKILL.md, etc.) before the agent processes them.
// The target branch may contain attacker-controlled files from a PR.
if h.SecurityEnabled() {
Expand Down
1 change: 1 addition & 0 deletions internal/harness/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ type Harness struct {
ValidationLoop *ValidationLoop `yaml:"validation_loop,omitempty"`
RunnerEnv map[string]string `yaml:"runner_env,omitempty"`
TimeoutMinutes int `yaml:"timeout_minutes,omitempty"`
ReadonlyRepo bool `yaml:"readonly_repo,omitempty"`
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
SandboxTimeoutSeconds int `yaml:"sandbox_timeout_seconds,omitempty"`
Security *SecurityConfig `yaml:"security,omitempty"`
AllowedRemoteResources []string `yaml:"allowed_remote_resources,omitempty"`
Expand Down
27 changes: 27 additions & 0 deletions internal/harness/harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,33 @@ agent: agents/code.md
assert.Nil(t, h.MaxRuntimeFetches)
}

func TestLoad_ReadonlyRepoField(t *testing.T) {
content := `
agent: agents/review.md
readonly_repo: true
`
dir := t.TempDir()
path := filepath.Join(dir, "test.yaml")
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))

h, err := Load(path)
require.NoError(t, err)
assert.True(t, h.ReadonlyRepo)
}

func TestLoad_ReadonlyRepoFieldOmitted(t *testing.T) {
content := `
agent: agents/code.md
`
dir := t.TempDir()
path := filepath.Join(dir, "test.yaml")
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))

h, err := Load(path)
require.NoError(t, err)
assert.False(t, h.ReadonlyRepo)
}

// --- ValidForgePlatform tests ---

func TestValidForgePlatform(t *testing.T) {
Expand Down
3 changes: 2 additions & 1 deletion internal/scaffold/fullsend-repo/agents/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ This agent has three skills. Select based on invocation context:
paths, scope authorization, PR body injection defense), and
produces a structured review result. Sub-agent definitions live in
`skills/pr-review/sub-agents/`. Each sub-agent is dispatched with
`model` from its frontmatter and `subagent_type: Explore`.
arguments appropriate to the "Agent" tool, as specified in the
pr-review skill.
- **`code-review`** — the prompt is about a local branch diff with
no PR, or another skill is delegating code evaluation. This skill
evaluates the diff and source files directly across the original
Expand Down
1 change: 1 addition & 0 deletions internal/scaffold/fullsend-repo/harness/review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ doc: docs/agents/review.md
model: opus
image: ghcr.io/fullsend-ai/fullsend-code:latest
policy: policies/review.yaml
readonly_repo: true
Comment thread
ben-alkov marked this conversation as resolved.

role: review
slug: fullsend-ai-review
Expand Down
2 changes: 1 addition & 1 deletion internal/scaffold/fullsend-repo/policies/review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ version: 1
# handles mutations on the runner with a separate write-scoped token.

filesystem_policy:
include_workdir: true
include_workdir: false
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
Comment thread
ben-alkov marked this conversation as resolved.
read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log]
read_write: [/sandbox, /tmp, /dev/null]
landlock:
Expand Down
41 changes: 24 additions & 17 deletions internal/scaffold/fullsend-repo/skills/pr-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,8 @@ For each selected sub-agent, assemble a context package containing:
For each selected sub-agent:

1. Read the sub-agent definition from `sub-agents/{name}.md`
2. Extract the `model` from frontmatter
3. Compose the spawn prompt from three parts:
2. Extract `model` and `name` from frontmatter
3. Compose the spawn prompt from:

**Part 1 — Sub-agent definition:** the full markdown body of the
sub-agent file (everything after the frontmatter)
Expand Down Expand Up @@ -360,11 +360,13 @@ For each selected sub-agent:
```

4. Spawn via Agent tool with:
- `name`: from the sub-agent frontmatter
- `tools`: `Read, Grep, Glob`
- `model`: from the sub-agent frontmatter (any value accepted by
Comment thread
ben-alkov marked this conversation as resolved.
the Agent tool's `model` parameter)
- `subagent_type`: `Explore` (read-only — sub-agents do not write)
- `run_in_background`: `true`
- `prompt`: composed from parts 1–5
the Agent tool's `model` parameter)
- `permissionMode`: `dontAsk`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[important] Are these parameter names verified against a live Agent tool call? The schema I can see has prompt, run_in_background, subagent_type, description — the new ones (name, tools, permissionMode, background, initialPrompt) don't appear. If we're swapping one set of unverified params for another, we should confirm they actually work before merging.

Also — the challenger dispatch at step 6d (~line 495) still uses the old subagent_type: Explore / prompt params. Worth updating to match.

@ben-alkov ben-alkov Jun 17, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified in the Anthropic docs https://code.claude.com/docs/en/sub-agents#supported-frontmatter-fields

EDIT: I keep forgetting that you guys don't necessarily read commit messages 😁

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which schema?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated challenger

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Agent tool's own JSON schema — you can see it in any Claude Code session by looking at what parameters the Agent tool accepts. It takes description, prompt, subagent_type, run_in_background, resume, and isolation.

The page you linked (code.claude.com/docs/en/sub-agents#supported-frontmatter-fields) lists frontmatter fields for agent definition .md files — name, tools, permissionMode, initialPrompt, etc. These go in the .md file's YAML header, not in the Agent tool call.

So there are two interfaces here, and I think the PR is crossing them:

  • Agent tool call: the orchestrator calls the Agent tool with prompt, subagent_type, run_in_background
  • Sub-agent definition frontmatter: the .md file declares name, tools, permissionMode, etc.

The old SKILL.md used valid Agent tool params (subagent_type: Explore, run_in_background: true, prompt). The new version uses frontmatter fields as if they were Agent tool params.

ADR 0027 in this repo covers the distinction — tools in frontmatter restricts what a subagent can access, but it's a property of the definition file, not something you pass to the Agent tool.

If the goal is to restrict sub-agent tools to Read, Grep, Glob (which I think is a good idea), we could add tools: [Read, Grep, Glob] to the sub-agent .md frontmatter and keep using valid Agent tool params for the invocation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the goal is to restrict sub-agent tools to Read, Grep, Glob (which I think is a good idea), we could add tools: [Read, Grep, Glob] to the sub-agent .md frontmatter

Agreed, but I'd take it a step further: IMO, all of that argument marshalling in the skill is wasted tokens anyway; Claude Code knows how to invoke subagents (using the Agent tool, which presumably Claude Code knows how to populate from the subagent definitions) - all we need to do is instruct it to build up the prompt the way we want.

- `background`: `true`
- `initialPrompt`: composed from parts 1–5

**All sub-agents MUST be dispatched simultaneously** — include all
Agent calls in a single message so they run concurrently. This is the
Expand Down Expand Up @@ -459,8 +461,9 @@ fresh context. The challenger has not seen the orchestrator's synthesis
— it receives only the raw findings and the diff, preserving context
isolation.

1. Read `sub-agents/challenger.md` for the sub-agent definition
2. Compose the spawn prompt from:
1. Read the sub-agent definition from `sub-agents/challenger.md`
2. Extract `model` and `name` from frontmatter
3. Compose the spawn prompt from:

**Part 1 — Sub-agent definition:** the full markdown body of the
challenger sub-agent file (everything after the frontmatter)
Expand Down Expand Up @@ -494,10 +497,14 @@ isolation.
REVIEW_SUB_AGENT_TRUE
```

3. Spawn via Agent tool with:
- `model`: from the challenger sub-agent frontmatter (`opus`)
- `subagent_type`: `Explore` (read-only)
- `prompt`: composed from parts 1–4
4. Spawn via Agent tool with:
- `name`: from the challenger sub-agent frontmatter
- `tools`: `Read, Grep, Glob`
- `model`: from the sub-agent frontmatter (any value accepted by
the Agent tool's `model` parameter)
- `permissionMode`: `dontAsk`
- `background`: `true`
- `initialPrompt`: composed from parts 1–4

**Prompt size guard:** If the combined context package (findings
JSON + diff + file list + PR metadata) exceeds 80 000 tokens,
Expand All @@ -510,7 +517,7 @@ isolation.
needs their findings as input), so it is dispatched sequentially,
not in the parallel batch from step 4.

4. Consume the challenger's output. The challenger returns a **different
5. Consume the challenger's output. The challenger returns a **different
format** from dimension sub-agents: an object with
`adjudicated_findings` and `removed_findings` arrays (not a flat
finding array). Parse accordingly:
Expand All @@ -522,15 +529,15 @@ isolation.
part of the standard finding schema.
- If `adjudicated_findings` is empty but the pre-challenger finding
set was non-empty, treat this as a challenger failure (fall back
per step 5 below). A legitimate challenger pass that removes all
findings is unlikely — an empty result more likely indicates a
parsing error or context truncation.
per the immediate next step below). A legitimate challenger pass
that removes all findings is unlikely — an empty result more likely
indicates a parsing error or context truncation.
- Otherwise, replace the merged finding set with the challenger's
`adjudicated_findings`.
- Log any `removed_findings` for transparency but do not include
them in the final review.

5. If the challenger sub-agent fails (timeout, error, empty
6. If the challenger sub-agent fails (timeout, error, empty
response), fall back to using the pre-challenger merged finding
set from steps 6a–6c. Record an **info**-level finding:

Expand Down
Loading