Shared Agent Context¶
Shared Agent Context (SAC) is the shipped local-first entry point for a piece of work: a file-backed workspace, a bounded Facts / Work / Know-how (FWK) view, and a reviewed proposal path that promotes a completed session into wiki, memory, or skills. It stores workspace-relative references, not copies of those owners, and it never becomes a second wiki, Flow tracker, or session store.
The domain lives in src/sac/. Operators use keryx workspace. Editor agents
use MCP sac.* on local stdio only. A live keryx shell turn can read FWK
through workspace_overview / workspace_read.
What is still experimental
The learned candidate policy is disabled by default (enabled: false,
killSwitch: true) and only synthetic fixtures exist. Phase 6b re-ingestion
of real receipts/outcomes is not shipped. Core registry, FWK reads,
propose/review, owner writers, and the access-receipt ledger are shipped
(phases 0–5 and 6a, on main since v0.2.32, present in 0.2.35).
Contracts and schemas:
docs/requirements/shared-agent-context/.
This page describes current behavior. Where it disagrees with a
future/planned sentence in an older requirements revision or in satellite
RP-01…RP-12 packages, this page and src/sac/ win.
The FWK model¶
SAC never blurs these three kinds:
- Facts — evidence-linked, task-local, freshness-bound statements. A Fact never silently becomes long-term knowledge.
- Work — a read-only projection of an existing Flow (done, next, blocked, verification evidence). SAC never creates a second tracker. Unbound work is explicit when no Flow is linked.
- Know-how — accepted wiki, memory, or skill items. Raw transcripts and hidden reasoning are not Know-how. Graph is not a knowledge owner.
Create a workspace and register resources¶
Work from a project cwd that already has .metaproject/. Identity is the local
OS user (user:local-<uid>). There is no --actor flag; adapters pass
request: undefined. There is no session↔workspace auto-bind: every later
call takes an explicit workspaceId.
Discover the full surface with keryx workspace --help. keryx commands
intentionally omits this verb (Phase 1 local-CLI only).
keryx workspace --help
keryx workspace create --title "Payments retry" --component ./src/payments
keryx workspace list
keryx workspace add-resource <workspace-id> --kind evidence --uri ./src/payments/retry.ts
keryx workspace add-resource <workspace-id> --kind flow --uri ./.metaproject/flows/<flow>/flow.json
keryx workspace show <workspace-id>
--component and --uri must be workspace-relative (./…). Absolute, network,
and escaping paths are rejected. add-resource takes --kind, --uri, and
optional --revision — not a positional typed-ref.
Read a bounded overview, then the detail¶
keryx workspace overview <workspace-id> --max-items 20 --max-tokens 2000
keryx workspace read <workspace-id> <item-id>
keryx workspace overview <workspace-id> --explain # human text on stderr; JSON stays on stdout
Defaults: overview maxItems=32 / maxTokens=4096; read maxItems=1 /
maxTokens=4096. If a mandatory item cannot fit the budget the operation
returns typed context_overflow and no access receipt. Optional omissions
are listed. Domain denials (freshness: "denied") are ordinary JSON, not a
tool/CLI crash.
Each allowed or denied progressive access appends a metadata-only receipt to
.metaproject/context-operations/access-receipts.jsonl (hash-chained;
src/sac/receipt-integrity.ts). Receipts do not store retrieved content,
prompts, or hidden reasoning. FWK results are derived response objects — there
is no persisted fwk-receipt.json.
Propose knowledge on wrap-up, and review it¶
Propose only from a completed keryx session (≥ archived messages). Kinds:
decision, wiki-update, memory-entry, follow-up, contract-change,
risk. --note is a one-line sidecar beside the proposal; it is not
evidence and is outside the frozen schema (known unbound field).
keryx workspace propose <workspace-id> --kind wiki-update \
--session <session-id> \
--note "Retry uses capped exponential backoff"
keryx workspace review <workspace-id> <proposal-id> --decision accepted \
--reason "owner accepted" \
--idempotency-key <key>
Review decisions: accepted | rejected | dismissed. Accept is possible only
through the harness composition (createHarnessProposalLifecycleService): it
wires real owner writers. The local factory is fail-closed and cannot
self-accept. --decision accepted additionally requires --confirm-token,
minted only by keryx workspace confirm-review <workspace-id> <proposal-id>
run yourself in a real, approval-gated shell. No MCP tool or agent-native
tool mints one. That is friction, not proof. The approval prompt is triggered
by the command's text, so a differently spelled command avoids it. The token
store is a plain file, so a shell-capable agent can forge it. An agent outside
keryx supervision is not gated at all. See
TM-03 for the full list. Accepting a wiki-update or
memory-entry proposal also returns a DedupHint (duplicates/conflicts
against already-accepted entries, computed after the decision, never
gating it) and, when the hint is non-empty, an optional model-judge
annotation — both informational only; nothing reads .verdict to decide
accept/reject. sac.review (MCP) and keryx workspace review (CLI) return
the identical shape.
| Kind | Owner | Lands under |
|---|---|---|
wiki-update |
wiki | .metaproject/wiki/decisions/ |
memory-entry |
memory | .metaproject/memory/ (same guarded seam as keryx memory new) |
decision, follow-up, contract-change, risk |
skill | .metaproject/project-skills/sac/<proposal-id>/ |
A failed owner write becomes stale, never accepted. The same
--idempotency-key replays the original terminal event. Flow wrap-up as a
propose source is not wired — only source: "session".
When the proposal trips the security scanner¶
confirm-review reads the proposal's security gate before it mints anything.
On a clean proposal it mints and says nothing further. When the gate is
needs-approval it prints what the scan found and in which evidence, and
then refuses:
keryx workspace confirm-review <workspace-id> <proposal-id>
# → security findings in this proposal's evidence:
# … findings and the evidence refs they came from …
# Read the evidence, then re-run with --acknowledge-security to state that you have.
keryx workspace confirm-review <workspace-id> <proposal-id> --acknowledge-security
The flag is the record that a human read the findings, and the minted token
carries that fact forward: review --decision accepted refuses a token that
does not. A clean proposal claims no acknowledgement even if the flag is
passed, and a proposal whose gate cannot be read is refused outright rather
than assumed to have passed — an unreadable gate is not a passing one.
The history is worth stating, because two releases got this wrong in opposite
directions. Before 0.2.75 no flag existed and the acceptance path passed no
acknowledgement at all, so a needs-approval proposal could not be accepted by
any route — a dead end, but a visible one. 0.2.74 replaced that by passing the
acknowledgement unconditionally on every call, which made the refusal unfirable
while its error text still promised "explicit human acknowledgement": a silent
bypass, the worse of the two. 0.2.75 made the acknowledgement real.
The same acknowledgement is available inside keryx shell. The /review
modal's Detail tab lists a proposal's actions as [a] accept, [s] accept with
the security acknowledgement, and [d] decline — each armed by its key and then
confirmed with [y]. [s] mints through the very same confirm-review
--acknowledge-security path, and it is never an automatic fallback for [a]:
with no acknowledgement handler wired, [s] reports itself unavailable instead
of downgrading to a plain accept.
Collaboration is read-only on the shipped adapters:
CollaborationService.record has no production CLI/MCP caller. Collaboration
and proposal lifecycle both append activity.jsonl with incompatible event
shapes — a mixed file can make collaboration overview fail.
Catch up on what accumulated during unattended runs¶
A pull-based, cwd-scoped digest: pending proposals, blocked runs,
unbound-candidate wrap-ups, and sessions of genuinely unknown fate — always
four separate sections, never merged. It also includes a fifth,
--include-lifecycle-flags section (shown by default) — every workspace,
memory entry, and wiki decision page whose recorded module no longer
resolves in the code graph, reusing the graph-diff signal that already
drives orphaned-wiki-page pruning. Report-only: it never archives a
workspace, edits a memory entry, or removes a wiki page on its own; a
workspace can carry both a pending proposal and a lifecycle flag at once.
Agent surfaces (MCP and shell)¶
MCP tools (src/mcp/tools.ts), local stdio / in-process only. HTTP returns
{ code: "sac_transport_denied" } before workspace discovery:
| Tool | Mutating |
|---|---|
sac.overview |
no |
sac.read |
no |
sac.collaboration |
no |
sac.propose |
yes |
sac.review |
yes |
Harness tools on a local keryx shell agent turn. They are not on
keryx serve / chat-only mode. Only the parent turn has them — a dispatched
subagent never gets workspace resolve/create/propose access.
| Tool | Risk | Same service as |
|---|---|---|
workspace_overview |
read | keryx workspace overview |
workspace_read |
read | keryx workspace read |
workspace_list |
read | keryx workspace list |
workspace_show |
read | keryx workspace show |
workspace_create |
mutating | keryx workspace create |
CLI and MCP reads share normalizeFwkResult. Never-shipped names
(workspace.fwk, workspace.get, workspace.proposal create --from-flow)
are not commands.
On-disk layout¶
.metaproject/workspaces/<workspace-id>/
workspace.json
proposals/<id>.json
proposals/<id>.<hash>.{decision,approval,write-intent,write-result}.json
activity.jsonl
session-evidence/<sessionId>.md
.metaproject/context-operations/
access-receipts.jsonl
access-receipts.checkpoint.json
workspace.json is the only SAC primary record. Knowledge bodies stay in
wiki / memory / project-skills.
Advanced: phase-6 runtime opt-in policy (off by default)¶
By default SAC uses a deterministic baseline policy. An optional learned
candidate sits behind resolvePolicySelection. It activates only when an
explicit pinned config and a complete integrity chain succeed; any failure is
fail-closed back to the baseline.
Config path: .metaproject/context-operations/policy-experiment/config.json.
{
"enabled": false,
"killSwitch": true,
"candidateArtifactRef": "./fixtures/sac-policy-experiment/artifacts/candidate.json",
"candidateArtifactDigest": "<sha256>",
"candidateVersion": "<immutable-version>",
"baselineArtifactRef": "./fixtures/sac-policy-experiment/artifacts/deterministic-baseline.json",
"baselineArtifactDigest": "<sha256>",
"baselineVersion": "<immutable-version>",
"corpusRef": "./fixtures/sac-policy-experiment/corpus.json",
"corpusDigest": "<sha256>",
"corpusVersion": "<immutable-version>",
"evaluationReportRef": "./fixtures/sac-policy-experiment/evaluation-report.json",
"evaluationDigest": "<sha256>"
}
Rules the guard enforces:
- Off by default. The candidate is selected only when
enabledistrueandkillSwitchisfalse; a missing config keeps the baseline. - Explicit pins. Every artifact needs a workspace-relative ref, a matching
sha256digest and an immutable version;latest/main/head-style versions are rejected. - Fixed-order integrity chain. Baseline → candidate → corpus → evaluation report → deterministic activation. Any parse error, digest mismatch, schema failure or activation mismatch falls back to the baseline.
- Rollback. Rolling back forces
enabled: falseandkillSwitch: true.
Before flipping enabled: true, verify the chain (validates pins even while
disabled; exits non-zero when integrityReady is false):
This path does not change any public CLI or MCP schema and never enables the candidate implicitly. The operator process for real (non-synthetic) artifacts is in the Phase 6b operator playbook; runtime re-ingestion of raw receipts/outcomes is still planned — see phase-6-real-opt-in-readiness.md.
Autonomous binding on an action-intent turn (Slate v2)¶
An action-intent turn in keryx shell/TUI/harness run — or an explicit
/goal <text> [--workspace <id>] [--auto [N]] (harness run
--goal ... [--workspace <id>] non-interactively) — now resolves-or-creates
its workspace binding automatically, by the same tool-calling judgment
ask_user/spawn_subagent already use (no new similarity/embedding
engine), and re-evaluates that binding mid-session if the topic shifts. On
completion it dispatches its own wrap-up proposal without a manual
propose call. This is still source: "session" under the hood, not a new
propose source — see the Flow-wrap-up note above. Review/accept is
unaffected: still strictly human, still gated by confirm-review.
Not shipped (do not treat as current)¶
- Propose from a Flow wrap-up snapshot.
- SAC over MCP HTTP or
keryx serve. - Public collaboration writer / member / archive APIs.
- Phase 6b real-data re-ingestion.
- Satellite RP-01…RP-12 capabilities.
Source reads of workspace files require POSIX openat + O_NOFOLLOW (macOS /
Linux). There is no Win32 fallback for that path.