Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0a6e5c0
delegate: a program's ceilings and its report ending are its own, not…
ZeroPoint95 Oct 5, 2026
7730417
security-audit: sec-af, the security auditor, built into codeaf
ZeroPoint95 Oct 5, 2026
662bc88
security-audit: the fixes its first real runs asked for
ZeroPoint95 Oct 5, 2026
057016a
security-audit: titled by what it audits, quieter page, change entry
ZeroPoint95 Oct 5, 2026
6338d7e
sec: security-audit is called sec
ZeroPoint95 Oct 5, 2026
f5568bc
sec: one vocabulary on its page, and a report in the account's words
ZeroPoint95 Oct 5, 2026
d35442a
sec: the guide constant and change entry the rename left unstaged
ZeroPoint95 Oct 5, 2026
ceb0825
sec: its files and SARIF say sec, and its hunt says which hunter foun…
ZeroPoint95 Oct 5, 2026
5e100b2
a program's ending reaches the person even when the reply to it fails
ZeroPoint95 Oct 5, 2026
1e0e372
sec: a run nobody limited gets four hours, not two
ZeroPoint95 Oct 5, 2026
04dd61f
sec: a proposed run reads the words it was asked with, and its row po…
ZeroPoint95 Oct 5, 2026
5bfaa6c
notices: the modules sec brought in
ZeroPoint95 Oct 5, 2026
732545c
config: CODEAF_RECORDS is launch plumbing
ZeroPoint95 Oct 5, 2026
62f603a
changelog: sec's entry carries its pull request's number
ZeroPoint95 Oct 5, 2026
eef127a
sec: the review's fixes — the entry's number, ABSORB's path, a person…
ZeroPoint95 Oct 5, 2026
11d0b3b
agentsession: sec's agent loop moves out of secaf, and names each pro…
ZeroPoint95 Oct 6, 2026
19c219f
sec and agentsession: an ending says sec's name once, and one empty r…
ZeroPoint95 Oct 6, 2026
df4e989
sec: the manual's ceiling endings are the two codeaf prints
ZeroPoint95 Oct 6, 2026
1453508
agentsession and sec: each agent's turns and time are its own
ZeroPoint95 Oct 6, 2026
ab7b005
agentsession: a long line says how to reach its rest, and grep shows …
ZeroPoint95 Oct 6, 2026
bedb052
agentsession: machinery only — every figure is the program's, and an …
ZeroPoint95 Oct 6, 2026
46b7dd3
Merge branch 'dev' into zeropoint95/sec
ZeroPoint95 Oct 6, 2026
9acd405
agentsession, ABSORB: /pr is called /review
ZeroPoint95 Oct 6, 2026
6aec350
Merge remote-tracking branch 'origin/dev' into zeropoint95/sec
ZeroPoint95 Oct 7, 2026
f8c5fb8
prefix: sec's guide costs both prefixes 396 bytes on today's dev
ZeroPoint95 Oct 7, 2026
a89aa4e
Merge remote-tracking branch 'origin/dev' into zeropoint95/sec
ZeroPoint95 Oct 7, 2026
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
  •  
  •  
  •  
27 changes: 27 additions & 0 deletions PERF.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,24 @@ What made dev about eight megabytes heavier than `main` on linux/amd64 by
the CI size job) is not attributed here; #1694 finds it, cuts what is redundant,
and lowers this number in the same commit as each cut.

It was reset a seventh time on 2026-10-05, when sec — sec-af, the
security auditor, copied in once at its tag `codeaf-absorb` — became the second
program built into codeaf (`internal/secaf`). Like senior-dev's, this one is a
decision: the programs codeaf hands a whole task to are built into every build.
Measured on darwin/amd64 with its own furrow artifact staged, with the flags
`make build` uses, the same tree with and without the program in the build's
list (`internal/delegate/builtin`), on Go 1.27.0:

| platform | without | with | what sec cost |
| --- | --- | --- | --- |
| darwin/amd64, furrow staged | 67,051,312 | 69,564,304 | 2,512,992 |

About 1.4 megabytes of it is sec-af's own code and the schema and YAML readers
it brings (`invopop/jsonschema`, `santhosh-tekuri/jsonschema`, `yaml/v4`); the
rest is the type and line tables that code carries. The budget rises by exactly
that cost, to 71,363,000 — this change's bill and nothing else. The tree without
it was 1,798,688 under the sixth reset's 68,850,000, and still is.

## Adaptive run shutdown grace

`Agent.Close` cancels adaptive runs and their name calls, then gives all accepted
Expand Down Expand Up @@ -1482,6 +1500,15 @@ fixed cap is **57,218** bytes, exactly 94 above the previous measurement.
The lean cap remains **49,590** bytes. The dated fixed waiver in
`prefixWaivers` pays only that measured increase.

**sec, the second program (sec-af's security audit), adds its guide (2026-10-05).** A program
costs the fixed prefix one item of the hand-off paragraph's list — its guide, at
most 400 bytes — and nothing else: its manual page and the wording of the turn its
report wakes ride no request. sec's guide is 386 bytes, and the caps
rise by exactly what it measured: against dev on 2026-10-07 (baa2d7f0f) it costs
both prefixes 396 bytes, so the full cap is **57,614** bytes and the lean cap
**49,986**, both dated in `prefixWaivers`. (Against the dev of 2026-10-05 it cost
the lean prefix only 229, before the page carried a program's guide there too.)

## Following through on a completion claim

A turn may decline handoff once per request when its own continuation says no
Expand Down
2 changes: 1 addition & 1 deletion SIZE-BUDGET
Original file line number Diff line number Diff line change
@@ -1 +1 @@
68850000
71363000
731 changes: 651 additions & 80 deletions THIRD-PARTY-NOTICES.md

Large diffs are not rendered by default.

56 changes: 42 additions & 14 deletions cmd/codeaf/carried.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,21 @@ func runCarriedHost(ctx context.Context, inv *delegate.Invocation) error {
strings.Join(foldSynopsis("codeaf "+inv.Program.Name+" "+carriedSynopsis), "\n"), inv.Program.Name)
return exitCannotRun
}
// THE PROGRAM SAYS WHICH OF ITS FLAGS NAMES ITS MODELS
// ([delegate.Delegate.ModelFlag]); codeaf resolves what was typed there.
modelFlag := strings.TrimSpace(inv.Program.ModelFlag)
asked := ""
if modelFlag != "" {
asked = strings.TrimSpace(inv.ExplicitFlags[modelFlag])
}
road, err := carriedModels()
if err != nil {
if word := strings.TrimSpace(inv.ExplicitFlags["high"]); word != "" && errors.Is(err, config.ErrNoAPIKey) {
if word := asked; word != "" && errors.Is(err, config.ErrNoAPIKey) {
return session.ProgramShellModelRefusal(word)
}
return err
}
if word := strings.TrimSpace(inv.ExplicitFlags["high"]); word != "" && road.resolveModel != nil {
if word := asked; word != "" && road.resolveModel != nil {
resolved, err := road.resolveModel(word)
if err != nil {
return err
Expand All @@ -259,9 +266,9 @@ func runCarriedHost(ctx context.Context, inv *delegate.Invocation) error {
}
resolved = strings.Join(models, ",")
}
inv.Line = carriedResolvedHigh(inv.Line, resolved)
inv.Line = carriedResolvedModel(inv.Line, modelFlag, resolved)
}
if strings.TrimSpace(inv.ExplicitFlags["high"]) == "" && road.defaultSeat != nil {
if asked == "" && road.defaultSeat != nil {
road.seat, err = road.defaultSeat()
if err != nil {
return err
Expand Down Expand Up @@ -390,8 +397,8 @@ func runCarriedHost(ctx context.Context, inv *delegate.Invocation) error {
Args: carriedInFolder(carriedChildLine(inv), inv, folder),
// NO PROVIDER KEY IS INHERITED BY THE PROGRAM (delegate.ChildEnv): the engine
// gets the loopback token it needs, and model commands lose that token.
Env: append(delegate.ChildEnv(api.API()), "SENIOR_DEV_IGNORED_AT_START="+folder.IgnoredFile(),
gitidentity.InputsEnv+"="+folder.InputsFile()),
Env: append(append(delegate.ChildEnv(api.API()), "SENIOR_DEV_IGNORED_AT_START="+folder.IgnoredFile(),
gitidentity.InputsEnv+"="+folder.InputsFile()), delegate.RecordsEnv(record)...),
Dir: here,
StderrPath: filepath.Join(record, carriedStderrName),
Grace: grace,
Expand Down Expand Up @@ -562,23 +569,23 @@ func carriedSeatedLine(inv *delegate.Invocation, seat string) []string {
return append(line, inv.Line[at:]...)
}

// carriedResolvedHigh replaces only the model flag's value on the person's
// carriedResolvedModel replaces only the model flag's value on the person's
// line. Every other program flag and every word of the brief stays as typed.
func carriedResolvedHigh(line []string, model string) []string {
func carriedResolvedModel(line []string, flag, model string) []string {
resolved := append([]string(nil), line...)
for i, word := range resolved {
if word == "--" {
break
}
switch {
case word == "--high" || word == "-high":
case word == "--"+flag || word == "-"+flag:
if i+1 < len(resolved) {
resolved[i+1] = model
}
case strings.HasPrefix(word, "--high="):
resolved[i] = "--high=" + model
case strings.HasPrefix(word, "-high="):
resolved[i] = "-high=" + model
case strings.HasPrefix(word, "--"+flag+"="):
resolved[i] = "--" + flag + "=" + model
case strings.HasPrefix(word, "-"+flag+"="):
resolved[i] = "-" + flag + "=" + model
}
}
return resolved
Expand Down Expand Up @@ -718,7 +725,11 @@ func (v *carriedView) begin() {
if v.records != nil {
return
}
v.say("%s · working in %s · %s", v.inv.Program.Name, v.where(), v.inv.Ceilings.Summary())
if ceilings := v.inv.Ceilings.Summary(); ceilings != "" {
v.say("%s · working in %s · %s", v.inv.Program.Name, v.where(), ceilings)
} else {
v.say("%s · working in %s", v.inv.Program.Name, v.where())
}
// A COPY IS CUT FROM A COMMIT, so what the person had not committed is not
// in it, and a person at a shell is told so before the run spends a cent on
// work that needed it — as a conversation's receipt tells them.
Expand Down Expand Up @@ -1008,6 +1019,23 @@ func (v *carriedView) end(result delegate.Result, runErr error, limited bool, sp
line += "; its last stage was " + result.Reading.LastStage
}
v.say("%s", line)
case !v.inv.Program.LandsTree():
// A PROGRAM THAT ANSWERS IS READ FOR ITS ANSWER. Its ending's first
// line is what it came to, and the answer under it is the report the
// person ran it for — the account a conversation is handed — rather
// than what a program that edits code claimed and checked.
v.say("%s", carriedEnding(name, *terminal))
if answer := strings.TrimSpace(terminal.Deliverable()); answer != "" && answer != strings.TrimSpace(terminal.Message) {
v.say("")
for _, line := range strings.Split(answer, "\n") {
if line = strings.TrimRight(line, " "); line == "" {
v.say("")
continue
}
v.say(" %s", line)
}
v.say("")
}
default:
v.say("%s", carriedEnding(name, *terminal))
if claim := terminal.Claim(); claim != "" {
Expand Down
2 changes: 1 addition & 1 deletion cmd/codeaf/carried_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestTheFrontPageFitsWithTheProgramsThisBuildCarries(t *testing.T) {

func TestSeniorDevShellFirstLineNamesItsEffectiveCeiling(t *testing.T) {
program := fakeCarriedProgram()
program.Name = "senior-dev"
program.Name, program.Unattended = "senior-dev", delegate.SeniorDevCeilings
for _, tc := range []struct {
line []string
want string
Expand Down
28 changes: 28 additions & 0 deletions docs/changes/unreleased/1781-sec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
kind: added
title: sec, sec-af's security audit built into codeaf, audits a repository or its changes
pr: 1781
surface: [chat, engine, docs, build]
invalidates:
- "senior-dev was the only program codeaf carries. There are two: sec (`/sec`, `codeaf sec`, `via: \"sec\"`) is the second, and it changes no files."
- "sec-af was a separate AgentField node with its own key and a coding-agent binary per agent call. It is copied into codeaf at sec-af's tag codeaf-absorb (47d57d7) as internal/secaf, runs only through codeaf, and is frozen in its own repository."
- "Every program's unattended ceilings were senior-dev's, chosen by its name in nine places. A program names its own (`Delegate.Unattended`); a program with none runs on what the conversation has left."
- "A program that lands text had no ending of its own and was sent looking for a branch. Its ending asks the chat for a summary and the program's own offer (`Delegate.FollowUp`) and starts nothing."
- "The model API answered 402 to any call its ceiling could not admit at that moment. A call held only by calls still in flight is answered 429 with Retry-After and X-Codeaf-Held; 402 is kept for a ceiling truly reached."
- "A shell run resolved a person's model only through a flag named `--high`. A program names its own model flag (`Delegate.ModelFlag`)."
- "A program the chat proposed was always handed the composed brief, which opens on `WHAT THE PERSON ASKED FOR`. A program whose brief is a few words it reads itself (`Delegate.Words`, sec's `changes since main thorough`) is handed those words alone, and a report program's row in the project's index points at its record folder, not at the repository."
- "A bare `/<program>` always asked for a brief. A program with a default brief runs bare (`/sec` audits the whole repository), and its row shows its own arguments."
- "sec-af failed every audit that named compliance frameworks, and its PR mode read the diff from the wrong folder and only filtered findings after a whole-repository scan. Frameworks map and report; an audit of the changes reads them in the folder under audit and tells the hunters what changed."
- "The fixed and lean prefix caps were 57,218 and 49,590 bytes and SIZE-BUDGET was 68,850,000. They are 57,614, 49,986 and 71,363,000, raised by exactly what sec measured (PERF.md)."
---

`/sec` (or `changes`, `changes since <ref>`, `quick`, `thorough`) starts an
audit that reads the folder with four read-only tools, hunts for vulnerabilities,
tests each one against the code, and reports what stands with file, line and fix.
Its report goes to the task's record folder (`sec-report.md`, `.json`, `.sarif`)
and its account to the conversation, which offers to hand confirmed findings to
senior-dev. Every model call goes through the run's model API, priced and held to
its ceiling: $5 and four hours unless the conversation has less.
Its agents run on `internal/agentsession`, a read-only agent loop over the run's
model API that any program codeaf carries can use, and that names each program's
own work in its prompt.
16 changes: 15 additions & 1 deletion docs/design/delegate/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,13 @@ one package codeaf's funnel law lets spell a model route. Every call:
1. is refused before it is made when the run's dollar ceiling is reached, with
HTTP 402 (a status senior-dev does not retry). A run a refusal ended is
reported as `<name> reached the run's dollar ceiling of $X: …`, whatever
status the program itself wrote, and ends on the run's cost limit;
status the program itself wrote, and ends on the run's cost limit. A call
that would fit once calls already in flight have given back what they
reserved is not refused but HELD (2026-10-05): HTTP 429 with `Retry-After`
and `X-Codeaf-Held: ceiling`, and no turn is written, because it was not
made and will be asked again. A program that makes many calls at once
(sec) waits it out; one that makes one call at a time never
meets it;
2. goes through codeaf's own model funnel, with its router, retries, caching and
billing, on the model the program asked for when one of the person's
services can serve it, and otherwise on the run's work seat, which the turn
Expand Down Expand Up @@ -261,6 +267,14 @@ record that named one, and its stage's word before any has.

## 8. Built in now for later programs

*sec-af became the second program on 2026-10-05: `sec`, in
`internal/secaf`, landing text (docs/design/security-audit/ABSORB.md). It is the
first program to make many calls at once and the first to land text, which is
why the held answer in §3, a program's own ceilings (`Unattended`), its follow-up
offer and its record folder (`CODEAF_RECORDS`) arrived with it. What follows is
as it was written on 2026-09-23.*


pr-af and sec-af, looked at on 2026-09-23, would need: plain structured calls
with `response_format`, many conversations at once (kept apart by thread),
grandchildren inheriting the API's address and token, quiet stretches of up to
Expand Down
84 changes: 84 additions & 0 deletions docs/design/security-audit/ABSORB.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Absorbing sec-af into codeaf

The security auditor was copied once from the sec-af repository and is frozen
there. This page records what was copied, what was left behind, and what was
changed on the way in, so a reader diffing `internal/secaf` against sec-af can
tell a deliberate change from drift.

## Source

- Repository: sec-af, Go module `github.com/Agent-Field/sec-af/go`
- Tag `codeaf-absorb`, commit `47d57d76ea170c44893d138862384476e3a617b8`
- Copied on 2026-10-05
- `go/internal/*` became `internal/secaf/*`; `go/docs/DESIGN.md` is
`SEC-AF-PORT-DESIGN.md` and `go/README.md` is `SEC-AF-GO-README.md` here,
both unchanged.

## Not copied

- `cmd/` (the HTTP node binary), `Dockerfile`, `docker-entrypoint.sh`,
`agentfield-package.yaml`, `Makefile`, `scripts/` (the Python golden and
schema generators).

## Deleted from the copy

- `internal/node`: the AgentField node: agent construction from the
environment, control-plane registration and serving, git cloning into a
workspaces directory, and the `audit` reasoner's HTTP status mapping. What
the in-process program needs moved to `internal/secaf/audit` (below).
- `internal/audit`: an uncalled one-field stub ported only so every Python
module had a counterpart. The name now belongs to the audit entry.
- `config/ai.go`'s provider machinery: provider selection, the external
CLI binaries and their paths, `ProviderEnv`, the eager `XDG_DATA_HOME`
directory, and every `SEC_AF_*` / `HARNESS_*` / `AI_MODEL` variable read.
`AuditConfig.Provider` (unread) went with it.
- The prompt drift test that compared `prompts/files` with the Python tree:
sec-af is frozen, so there is nothing left to drift from.
- Tests that only covered the deleted parts: node construction and env
precedence, cloning and workspace fallbacks, provider env, the SDK router's
`/discover` payload and router tags.

## Changed

- Imports were rewritten to `github.com/Agent-Field/codeaf/internal/secaf/...`.
- The AgentField SDK's `agent` and `harness` packages are no longer imported.
Only `sdk/go/ai` remains, at codeaf's pinned version; no `ai` symbol needed
an adaptation.
- `appx` and the agent loop codeaf wrote for the audit live outside the copy,
at `internal/agentsession/appx` and `internal/agentsession`, because they are
codeaf's and not sec-af's, and a second carried program (`/review`, being built on
its own branch) is to run on them too. The loop names the work in its
system prompt from `Config.Work`; sec's is `a security audit`.
- `appx` declares its own `HarnessOptions{Cwd, ProjectDir}` and
`HarnessResult{Result, Parsed, IsError, ErrorMessage, NumTurns, DurationMS,
CostUSD}` in place of the SDK's types. `CostUSD` stays a `*float64` because
the cost trackers skip an unreported cost.
- `reasoners.RegisterAll` fills an in-process `Registry` (name to handler,
plus input schema) instead of an SDK router. The 33 names, their order, and
the input validation are the same. A refused body is a
`*reasoners.InputError` carrying 422, where it was an SDK `ExecuteError`.
- `audit.WithLocalCalls` answers `.call` in process. It keeps the JSON round
trip in both directions and returns a failed call as an opaque error, as the
control-plane hop did.
- `audit.Run` resolves the repository before it builds the orchestrator, and
passes it in through the new `orch.NewAt`. The node built the orchestrator
against `SEC_AF_REPO_PATH` or its working directory first.
- Only an existing local directory is audited. A URL, a missing path or a
file is refused with a 400 `*audit.Error`, and nothing is cloned.
- `phases.NodeID()` is the constant `"sec-af"`; `NODE_ID` is no longer read.
- `config.AIIntegrationConfig` keeps `AIModel` and the retry schedule.
`DefaultAIConfig()` has the node's retry defaults (3 retries, 2s initial and
8s maximum backoff) and no model. With no model set, the AI gate passes no
`ai.WithModel`, so the App picks the model.
- The gates' `HarnessWrapper` (not on the live path) passes only cwd and
project_dir; its model, max-turns and budget overrides are gone.
- `harnessx.Extract` writes its diagnostic block to `harnessx.Diagnostics`
(default `io.Discard`) instead of stdout. The audit entry no longer prints
`AUDIT ERROR:` to stdout; the failure note carries the same text.
- `schemas.Verdict` was renamed `schemas.ExploitVerdict`, because codeaf
allows exactly one type named `Verdict` (the taxonomy law).
- Every `aforge`/`openaf` spelling was removed from Go sources; testdata keeps
its recorded values.
- `invopop/jsonschema` resolves to v0.14.0, the version codeaf's module graph
already selected (sec-af pinned v0.13.0). It is only the fallback schema
reflector, and every golden passes on it.
Loading
Loading