agy-worker-mcp 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/commands/agy-ceiling.md +31 -0
- package/dist/broker/blockers.d.ts +86 -0
- package/dist/broker/blockers.d.ts.map +1 -0
- package/dist/broker/blockers.js +390 -0
- package/dist/broker/blockers.js.map +1 -0
- package/dist/broker/outcome.d.ts +85 -0
- package/dist/broker/outcome.d.ts.map +1 -0
- package/dist/broker/outcome.js +147 -0
- package/dist/broker/outcome.js.map +1 -0
- package/dist/broker/reconcile.d.ts +74 -0
- package/dist/broker/reconcile.d.ts.map +1 -0
- package/dist/broker/reconcile.js +415 -0
- package/dist/broker/reconcile.js.map +1 -0
- package/dist/broker/result.d.ts +88 -0
- package/dist/broker/result.d.ts.map +1 -0
- package/dist/broker/result.js +234 -0
- package/dist/broker/result.js.map +1 -0
- package/dist/broker/verify.d.ts +52 -0
- package/dist/broker/verify.d.ts.map +1 -0
- package/dist/broker/verify.js +174 -0
- package/dist/broker/verify.js.map +1 -0
- package/dist/contract/errors.d.ts +119 -0
- package/dist/contract/errors.d.ts.map +1 -0
- package/dist/contract/errors.js +99 -0
- package/dist/contract/errors.js.map +1 -0
- package/dist/contract/index.d.ts +10 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +10 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/paths.d.ts +139 -0
- package/dist/contract/paths.d.ts.map +1 -0
- package/dist/contract/paths.js +359 -0
- package/dist/contract/paths.js.map +1 -0
- package/dist/contract/schema.sql +64 -0
- package/dist/contract/types.d.ts +1113 -0
- package/dist/contract/types.d.ts.map +1 -0
- package/dist/contract/types.js +114 -0
- package/dist/contract/types.js.map +1 -0
- package/dist/events/cursor.d.ts +65 -0
- package/dist/events/cursor.d.ts.map +1 -0
- package/dist/events/cursor.js +286 -0
- package/dist/events/cursor.js.map +1 -0
- package/dist/events/detect.d.ts +58 -0
- package/dist/events/detect.d.ts.map +1 -0
- package/dist/events/detect.js +183 -0
- package/dist/events/detect.js.map +1 -0
- package/dist/events/normalize.d.ts +30 -0
- package/dist/events/normalize.d.ts.map +1 -0
- package/dist/events/normalize.js +158 -0
- package/dist/events/normalize.js.map +1 -0
- package/dist/events/parse.d.ts +41 -0
- package/dist/events/parse.d.ts.map +1 -0
- package/dist/events/parse.js +89 -0
- package/dist/events/parse.js.map +1 -0
- package/dist/gate/bind.d.ts +28 -0
- package/dist/gate/bind.d.ts.map +1 -0
- package/dist/gate/bind.js +154 -0
- package/dist/gate/bind.js.map +1 -0
- package/dist/gate/gate.d.ts +139 -0
- package/dist/gate/gate.d.ts.map +1 -0
- package/dist/gate/gate.js +488 -0
- package/dist/gate/gate.js.map +1 -0
- package/dist/gate/hooks-file.d.ts +55 -0
- package/dist/gate/hooks-file.d.ts.map +1 -0
- package/dist/gate/hooks-file.js +104 -0
- package/dist/gate/hooks-file.js.map +1 -0
- package/dist/gate.d.ts +3 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +20 -0
- package/dist/gate.js.map +1 -0
- package/dist/policy/ceiling-review.d.ts +22 -0
- package/dist/policy/ceiling-review.d.ts.map +1 -0
- package/dist/policy/ceiling-review.js +170 -0
- package/dist/policy/ceiling-review.js.map +1 -0
- package/dist/policy/ceiling.d.ts +128 -0
- package/dist/policy/ceiling.d.ts.map +1 -0
- package/dist/policy/ceiling.js +273 -0
- package/dist/policy/ceiling.js.map +1 -0
- package/dist/policy/containment.d.ts +100 -0
- package/dist/policy/containment.d.ts.map +1 -0
- package/dist/policy/containment.js +1071 -0
- package/dist/policy/containment.js.map +1 -0
- package/dist/policy/hard-deny.d.ts +28 -0
- package/dist/policy/hard-deny.d.ts.map +1 -0
- package/dist/policy/hard-deny.js +78 -0
- package/dist/policy/hard-deny.js.map +1 -0
- package/dist/policy/profiles.d.ts +82 -0
- package/dist/policy/profiles.d.ts.map +1 -0
- package/dist/policy/profiles.js +317 -0
- package/dist/policy/profiles.js.map +1 -0
- package/dist/policy/rules.d.ts +138 -0
- package/dist/policy/rules.d.ts.map +1 -0
- package/dist/policy/rules.js +1180 -0
- package/dist/policy/rules.js.map +1 -0
- package/dist/policy/seatbelt.d.ts +32 -0
- package/dist/policy/seatbelt.d.ts.map +1 -0
- package/dist/policy/seatbelt.js +59 -0
- package/dist/policy/seatbelt.js.map +1 -0
- package/dist/policy/tools.d.ts +36 -0
- package/dist/policy/tools.d.ts.map +1 -0
- package/dist/policy/tools.js +94 -0
- package/dist/policy/tools.js.map +1 -0
- package/dist/runner/gate-watchdog.d.ts +93 -0
- package/dist/runner/gate-watchdog.d.ts.map +1 -0
- package/dist/runner/gate-watchdog.js +153 -0
- package/dist/runner/gate-watchdog.js.map +1 -0
- package/dist/runner/idle.d.ts +51 -0
- package/dist/runner/idle.d.ts.map +1 -0
- package/dist/runner/idle.js +81 -0
- package/dist/runner/idle.js.map +1 -0
- package/dist/runner/inbox.d.ts +56 -0
- package/dist/runner/inbox.d.ts.map +1 -0
- package/dist/runner/inbox.js +134 -0
- package/dist/runner/inbox.js.map +1 -0
- package/dist/runner/reap.d.ts +65 -0
- package/dist/runner/reap.d.ts.map +1 -0
- package/dist/runner/reap.js +168 -0
- package/dist/runner/reap.js.map +1 -0
- package/dist/runner/runner.d.ts +51 -0
- package/dist/runner/runner.d.ts.map +1 -0
- package/dist/runner/runner.js +289 -0
- package/dist/runner/runner.js.map +1 -0
- package/dist/runner/spawn.d.ts +98 -0
- package/dist/runner/spawn.d.ts.map +1 -0
- package/dist/runner/spawn.js +230 -0
- package/dist/runner/spawn.js.map +1 -0
- package/dist/runner/verify.d.ts +24 -0
- package/dist/runner/verify.d.ts.map +1 -0
- package/dist/runner/verify.js +108 -0
- package/dist/runner/verify.js.map +1 -0
- package/dist/runner.d.ts +3 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +15 -0
- package/dist/runner.js.map +1 -0
- package/dist/server/context.d.ts +41 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +59 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/instructions.d.ts +13 -0
- package/dist/server/instructions.d.ts.map +1 -0
- package/dist/server/instructions.js +121 -0
- package/dist/server/instructions.js.map +1 -0
- package/dist/server/server.d.ts +20 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +55 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/tools/cancel.d.ts +24 -0
- package/dist/server/tools/cancel.d.ts.map +1 -0
- package/dist/server/tools/cancel.js +59 -0
- package/dist/server/tools/cancel.js.map +1 -0
- package/dist/server/tools/capabilities.d.ts +36 -0
- package/dist/server/tools/capabilities.d.ts.map +1 -0
- package/dist/server/tools/capabilities.js +116 -0
- package/dist/server/tools/capabilities.js.map +1 -0
- package/dist/server/tools/ceiling.d.ts +31 -0
- package/dist/server/tools/ceiling.d.ts.map +1 -0
- package/dist/server/tools/ceiling.js +121 -0
- package/dist/server/tools/ceiling.js.map +1 -0
- package/dist/server/tools/index.d.ts +38 -0
- package/dist/server/tools/index.d.ts.map +1 -0
- package/dist/server/tools/index.js +115 -0
- package/dist/server/tools/index.js.map +1 -0
- package/dist/server/tools/listJobs.d.ts +30 -0
- package/dist/server/tools/listJobs.d.ts.map +1 -0
- package/dist/server/tools/listJobs.js +37 -0
- package/dist/server/tools/listJobs.js.map +1 -0
- package/dist/server/tools/logs.d.ts +31 -0
- package/dist/server/tools/logs.d.ts.map +1 -0
- package/dist/server/tools/logs.js +96 -0
- package/dist/server/tools/logs.js.map +1 -0
- package/dist/server/tools/result.d.ts +32 -0
- package/dist/server/tools/result.d.ts.map +1 -0
- package/dist/server/tools/result.js +95 -0
- package/dist/server/tools/result.js.map +1 -0
- package/dist/server/tools/send.d.ts +37 -0
- package/dist/server/tools/send.d.ts.map +1 -0
- package/dist/server/tools/send.js +71 -0
- package/dist/server/tools/send.js.map +1 -0
- package/dist/server/tools/sessions.d.ts +28 -0
- package/dist/server/tools/sessions.d.ts.map +1 -0
- package/dist/server/tools/sessions.js +54 -0
- package/dist/server/tools/sessions.js.map +1 -0
- package/dist/server/tools/start.d.ts +107 -0
- package/dist/server/tools/start.d.ts.map +1 -0
- package/dist/server/tools/start.js +482 -0
- package/dist/server/tools/start.js.map +1 -0
- package/dist/server/tools/wait.d.ts +29 -0
- package/dist/server/tools/wait.d.ts.map +1 -0
- package/dist/server/tools/wait.js +120 -0
- package/dist/server/tools/wait.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +17 -0
- package/dist/server.js.map +1 -0
- package/dist/setup/install.d.ts +52 -0
- package/dist/setup/install.d.ts.map +1 -0
- package/dist/setup/install.js +108 -0
- package/dist/setup/install.js.map +1 -0
- package/dist/setup.d.ts +3 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +24 -0
- package/dist/setup.js.map +1 -0
- package/dist/store/db.d.ts +49 -0
- package/dist/store/db.d.ts.map +1 -0
- package/dist/store/db.js +101 -0
- package/dist/store/db.js.map +1 -0
- package/dist/store/jobs.d.ts +45 -0
- package/dist/store/jobs.d.ts.map +1 -0
- package/dist/store/jobs.js +150 -0
- package/dist/store/jobs.js.map +1 -0
- package/dist/store/locks.d.ts +60 -0
- package/dist/store/locks.d.ts.map +1 -0
- package/dist/store/locks.js +172 -0
- package/dist/store/locks.js.map +1 -0
- package/dist/store/sessions.d.ts +37 -0
- package/dist/store/sessions.d.ts.map +1 -0
- package/dist/store/sessions.js +117 -0
- package/dist/store/sessions.js.map +1 -0
- package/dist/trace/digest.d.ts +77 -0
- package/dist/trace/digest.d.ts.map +1 -0
- package/dist/trace/digest.js +481 -0
- package/dist/trace/digest.js.map +1 -0
- package/package.json +68 -0
- package/skills/agy-ceiling/SKILL.md +72 -0
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen contract types for agy-worker-mcp.
|
|
3
|
+
*
|
|
4
|
+
* READ-ONLY after Stage 1. If a downstream module needs a change here, report it
|
|
5
|
+
* through `contract_change_requests` instead of editing this file.
|
|
6
|
+
*
|
|
7
|
+
* Everything describing agy's own wire format is transcribed from measured
|
|
8
|
+
* output and raw NDJSON transcripts. No field in the `Agy*` types is invented.
|
|
9
|
+
*/
|
|
10
|
+
/** Bumped whenever `schema.sql` changes shape. Stored in `meta.schema_version`. */
|
|
11
|
+
export declare const SCHEMA_VERSION = 1;
|
|
12
|
+
/**
|
|
13
|
+
* Shape version of `jobs/<id>/broker-result.json`, independent of the SQLite
|
|
14
|
+
* `SCHEMA_VERSION` (that file is not a table).
|
|
15
|
+
*
|
|
16
|
+
* 1 → 2: `verification.permission_denials` + `verification.environment_blocks`
|
|
17
|
+
* became the single `verification.blockers` list.
|
|
18
|
+
* 2 → 3: `verification.verify` was added (PR6, `verify_command`) — `null` on a
|
|
19
|
+
* migrated file, since no pre-0.2.0 job ever ran a verify command.
|
|
20
|
+
* `loadBrokerResult` migrates an older file in memory rather than failing, so a
|
|
21
|
+
* job directory written by an earlier release stays readable; an unknown
|
|
22
|
+
* *newer* version is refused loudly instead.
|
|
23
|
+
*/
|
|
24
|
+
export declare const BROKER_RESULT_VERSION = 3;
|
|
25
|
+
/** Environment variables this package reads. Nothing else may be consulted. */
|
|
26
|
+
export declare const ENV: {
|
|
27
|
+
/** Overrides project-root discovery (see docs/operations.md). */
|
|
28
|
+
readonly PROJECT_ROOT: "AGY_WORKER_PROJECT";
|
|
29
|
+
/** Overrides `~/.agy-worker` as the state home. Tests set this. */
|
|
30
|
+
readonly STATE_HOME: "AGY_WORKER_HOME";
|
|
31
|
+
/** Absolute path to the `agy` executable. Tests point this at the fake. */
|
|
32
|
+
readonly AGY_BIN: "AGY_WORKER_AGY_BIN";
|
|
33
|
+
/**
|
|
34
|
+
* Overrides `binPath('gate')`. Tests point this at a nonexistent path to
|
|
35
|
+
* exercise `agy_start`'s `existsSync(gatePath)` check (PR3 §3.1) without
|
|
36
|
+
* having to move the real `dist/gate.js` out from under a build every other
|
|
37
|
+
* test also depends on.
|
|
38
|
+
*/
|
|
39
|
+
readonly GATE_BIN: "AGY_WORKER_GATE_BIN";
|
|
40
|
+
/** Job id the runner process is executing. Set by the server when spawning. */
|
|
41
|
+
readonly JOB_ID: "AGY_WORKER_JOB_ID";
|
|
42
|
+
/** Scenario file consumed by `test/fake-agy` only; never read by src/. */
|
|
43
|
+
readonly FAKE_SCENARIO: "AGY_FAKE_SCENARIO";
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Flags that must never appear in a generated argv, at any call site.
|
|
47
|
+
* `--continue` resumes a *global* "most recent conversation" which collides
|
|
48
|
+
* head-on with multi-session operation; `--prompt-interactive` needs a TTY.
|
|
49
|
+
*
|
|
50
|
+
* `--dangerously-skip-permissions` was on this list until 0.2.0. It is now
|
|
51
|
+
* emitted on every spawn (`buildAgyArgv`) — see the comment there for the
|
|
52
|
+
* three invariants that make that safe and the measurement (M3) behind it.
|
|
53
|
+
*/
|
|
54
|
+
export declare const FORBIDDEN_AGY_FLAGS: readonly string[];
|
|
55
|
+
/** agy's own prefix on a hook denial, measured in `.spike/out/run6.events.ndjson`. */
|
|
56
|
+
export declare const HOOK_DENIAL_PREFIX = "tool call denied by pre-tool hook:";
|
|
57
|
+
/**
|
|
58
|
+
* Marker the gate embeds in its `reason` string so `events/detect.ts` can parse a
|
|
59
|
+
* denial losslessly instead of scraping prose. agy passes `reason` through to the
|
|
60
|
+
* model verbatim (measured, §10), so the human-readable guidance comes first and
|
|
61
|
+
* the machine payload trails it in brackets:
|
|
62
|
+
*
|
|
63
|
+
* `<guidance sentence> [agy-worker-denial:{"required_rule":"command(git push)",...}]`
|
|
64
|
+
*/
|
|
65
|
+
export declare const GATE_DENIAL_MARKER = "agy-worker-denial:";
|
|
66
|
+
/** Output signatures that mean "the sandbox blocked this silently" (see docs/permissions.md). */
|
|
67
|
+
export declare const ENVIRONMENT_BLOCK_SIGNATURES: readonly string[];
|
|
68
|
+
/**
|
|
69
|
+
* The subset of {@link ENVIRONMENT_BLOCK_SIGNATURES} that means "the sandbox
|
|
70
|
+
* refused the network" rather than a filesystem or capability refusal. Since
|
|
71
|
+
* 0.2.1 the two are lifted the same way — by not running the job sandboxed
|
|
72
|
+
* (I2) — so this only shapes the blocker's wording.
|
|
73
|
+
*/
|
|
74
|
+
export declare const NETWORK_BLOCK_SIGNATURES: readonly string[];
|
|
75
|
+
/**
|
|
76
|
+
* Substrings that identify a refusal by agy's **own** permission engine, as
|
|
77
|
+
* opposed to our gate.
|
|
78
|
+
*
|
|
79
|
+
* Both entries are verbatim fragments of a measured message; the observed line
|
|
80
|
+
* was:
|
|
81
|
+
*
|
|
82
|
+
* `permission check failed for unsandboxed "ls -la /Users/<user>/.jdks/": user
|
|
83
|
+
* denied permission to run command:\nls -la /Users/<user>/.jdks/`
|
|
84
|
+
*
|
|
85
|
+
* These refusals are not ours and no `permissions.allow` rule can lift them —
|
|
86
|
+
* the gate never saw the call. Matching them only changes how the warning is
|
|
87
|
+
* worded, never the outcome (they arrive as ordinary `state: 'ERROR'` tool
|
|
88
|
+
* steps, which `decideOutcome` deliberately does not count as blocks).
|
|
89
|
+
*/
|
|
90
|
+
export declare const AGY_ENGINE_REFUSAL_SIGNATURES: readonly string[];
|
|
91
|
+
/**
|
|
92
|
+
* Token accounting attached to `step_update` and `result`.
|
|
93
|
+
* `input_tokens` / `output_tokens` / `total_tokens` appear in every measured
|
|
94
|
+
* sample; `thinking_tokens` / `cache_read_tokens` appear in every raw capture but
|
|
95
|
+
* are not in the §4 excerpt, so they stay optional.
|
|
96
|
+
*/
|
|
97
|
+
export interface AgyUsage {
|
|
98
|
+
input_tokens: number;
|
|
99
|
+
output_tokens: number;
|
|
100
|
+
total_tokens: number;
|
|
101
|
+
thinking_tokens?: number;
|
|
102
|
+
cache_read_tokens?: number;
|
|
103
|
+
}
|
|
104
|
+
/** `tool_info.error` — the only structured failure signal agy emits (Class 1). */
|
|
105
|
+
export interface AgyToolError {
|
|
106
|
+
type: string;
|
|
107
|
+
message: string;
|
|
108
|
+
}
|
|
109
|
+
export interface AgyToolInfo {
|
|
110
|
+
name?: string;
|
|
111
|
+
parameters?: Record<string, unknown>;
|
|
112
|
+
output?: string;
|
|
113
|
+
error?: AgyToolError;
|
|
114
|
+
}
|
|
115
|
+
/** Measured values. Anything else must be treated as unknown, not as an error. */
|
|
116
|
+
export type AgyStepState = 'ACTIVE' | 'DONE' | 'ERROR';
|
|
117
|
+
export type AgyStepType = 'user_input' | 'agent_response' | 'tool' | 'system_message';
|
|
118
|
+
export type AgyResultStatus = 'SUCCESS' | 'ERROR';
|
|
119
|
+
/**
|
|
120
|
+
* First line of every run. `conversation_id` sits on the *envelope*, not inside
|
|
121
|
+
* `init` — this is what makes gate binding possible before the first tool call.
|
|
122
|
+
*/
|
|
123
|
+
export interface AgyInitEvent {
|
|
124
|
+
event: 'init';
|
|
125
|
+
conversation_id: string;
|
|
126
|
+
init: {
|
|
127
|
+
model: string;
|
|
128
|
+
cwd: string;
|
|
129
|
+
permission_mode: string;
|
|
130
|
+
tools: string[];
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export interface AgyStepUpdateEvent {
|
|
134
|
+
event: 'step_update';
|
|
135
|
+
step_update: {
|
|
136
|
+
conversation_id: string;
|
|
137
|
+
step_index: number;
|
|
138
|
+
state: AgyStepState | string;
|
|
139
|
+
step_type: AgyStepType | string;
|
|
140
|
+
tool_name?: string;
|
|
141
|
+
tool_info?: AgyToolInfo;
|
|
142
|
+
text_delta?: string;
|
|
143
|
+
duration_seconds?: number;
|
|
144
|
+
usage?: AgyUsage;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** One per turn. `num_turns` increases and survives `--conversation` resume. */
|
|
148
|
+
export interface AgyResultEvent {
|
|
149
|
+
event: 'result';
|
|
150
|
+
result: {
|
|
151
|
+
conversation_id: string;
|
|
152
|
+
status: AgyResultStatus | string;
|
|
153
|
+
response: string;
|
|
154
|
+
error?: string;
|
|
155
|
+
duration_seconds: number;
|
|
156
|
+
num_turns: number;
|
|
157
|
+
usage: AgyUsage;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export type AgyEvent = AgyInitEvent | AgyStepUpdateEvent | AgyResultEvent;
|
|
161
|
+
/** An envelope whose `event` we do not model. Preserved, never dropped. */
|
|
162
|
+
export interface AgyUnknownEvent {
|
|
163
|
+
event: string;
|
|
164
|
+
[key: string]: unknown;
|
|
165
|
+
}
|
|
166
|
+
/** stream-json *input*, one line = one turn (§5). Byte-identical to agy's schema. */
|
|
167
|
+
export interface AgyStreamUserInput {
|
|
168
|
+
event: 'user';
|
|
169
|
+
message: {
|
|
170
|
+
role: 'user';
|
|
171
|
+
content: Array<{
|
|
172
|
+
type: 'text';
|
|
173
|
+
text: string;
|
|
174
|
+
}>;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export type Lifecycle = 'queued' | 'starting' | 'running' | 'canceling' | 'finished';
|
|
178
|
+
/**
|
|
179
|
+
* Broker verdict. Distinct from `agent_status` on purpose: agy reports SUCCESS
|
|
180
|
+
* with exit 0 for both permission denials and sandbox blocks (measured, §9/§11),
|
|
181
|
+
* so its self-report can never produce `verified_success` on its own.
|
|
182
|
+
*/
|
|
183
|
+
export type Outcome =
|
|
184
|
+
/** Finished, and every check the broker could run actually passed. */
|
|
185
|
+
'verified_success'
|
|
186
|
+
/** Finished with no detected block, but nothing verifiable was requested. */
|
|
187
|
+
| 'success_unverified'
|
|
188
|
+
/** Ran to completion but a Class 1 denial or Class 2 environment block bit. */
|
|
189
|
+
| 'blocked'
|
|
190
|
+
/** agy reported ERROR, or exited non-zero. */
|
|
191
|
+
| 'failed'
|
|
192
|
+
/** `deadline_at` passed and the process group was killed. */
|
|
193
|
+
| 'timed_out'
|
|
194
|
+
/** `agy_cancel` killed it. */
|
|
195
|
+
| 'canceled'
|
|
196
|
+
/** Runner vanished without writing `exit_code`. */
|
|
197
|
+
| 'process_error'
|
|
198
|
+
/** Recorded pid is alive but is a different process (pid reuse). */
|
|
199
|
+
| 'orphaned';
|
|
200
|
+
/** agy's *unverified* self-report. Never drives `outcome` alone. */
|
|
201
|
+
export type AgentStatus = 'SUCCESS' | 'ERROR' | 'unknown';
|
|
202
|
+
/** Did the job honour the structured contract the caller asked for? */
|
|
203
|
+
export type ContractStatus = 'not_required' | 'satisfied' | 'violated' | 'unknown';
|
|
204
|
+
/** Built-in profiles (see docs/permissions.md). */
|
|
205
|
+
export type Profile = 'research_readonly' | 'general_worker';
|
|
206
|
+
export type SessionMode = 'oneshot' | 'session';
|
|
207
|
+
export type OnDenial = 'abort' | 'continue' | 'guide';
|
|
208
|
+
/** Lock rows actually stored in the `locks` table. */
|
|
209
|
+
export type LockScope = 'cwd_write' | 'session';
|
|
210
|
+
/**
|
|
211
|
+
* What an acquisition attempt was for. `running_limit` is the per-project
|
|
212
|
+
* concurrency ceiling — it is enforced inside the same
|
|
213
|
+
* `BEGIN IMMEDIATE` but owns no row, so it is not a `LockScope`.
|
|
214
|
+
*/
|
|
215
|
+
export type LockRequestScope = LockScope | 'running_limit';
|
|
216
|
+
export type SessionState = 'active' | 'closed';
|
|
217
|
+
/** Row shape of `jobs`. Column-for-column with `schema.sql`. */
|
|
218
|
+
export interface JobRow {
|
|
219
|
+
job_id: string;
|
|
220
|
+
session_id: string | null;
|
|
221
|
+
lifecycle: Lifecycle;
|
|
222
|
+
outcome: Outcome | null;
|
|
223
|
+
headline: string | null;
|
|
224
|
+
cwd: string;
|
|
225
|
+
profile: Profile;
|
|
226
|
+
/** 0 = read-only, 1 = write. Stored as INTEGER because SQLite has no boolean. */
|
|
227
|
+
write_mode: number;
|
|
228
|
+
session_mode: SessionMode;
|
|
229
|
+
pid: number | null;
|
|
230
|
+
pgid: number | null;
|
|
231
|
+
/** Opaque platform token guarding against pid reuse. Compared, never parsed. */
|
|
232
|
+
proc_start_time: string | null;
|
|
233
|
+
created_at: number;
|
|
234
|
+
started_at: number | null;
|
|
235
|
+
finished_at: number | null;
|
|
236
|
+
deadline_at: number | null;
|
|
237
|
+
exit_code: number | null;
|
|
238
|
+
agent_status: AgentStatus | null;
|
|
239
|
+
contract_status: ContractStatus | null;
|
|
240
|
+
on_denial: OnDenial;
|
|
241
|
+
requested_by: string | null;
|
|
242
|
+
parent_task_id: string | null;
|
|
243
|
+
}
|
|
244
|
+
/** Row shape of `sessions`. `conversation_id` is bound lazily from the first init event. */
|
|
245
|
+
export interface SessionRow {
|
|
246
|
+
session_id: string;
|
|
247
|
+
conversation_id: string | null;
|
|
248
|
+
cwd: string;
|
|
249
|
+
model: string | null;
|
|
250
|
+
effort: string | null;
|
|
251
|
+
profile: Profile | null;
|
|
252
|
+
turn_count: number;
|
|
253
|
+
last_job_id: string | null;
|
|
254
|
+
created_at: number;
|
|
255
|
+
last_used_at: number;
|
|
256
|
+
state: SessionState;
|
|
257
|
+
}
|
|
258
|
+
/** Row shape of `locks`. */
|
|
259
|
+
export interface LockRow {
|
|
260
|
+
scope: LockScope;
|
|
261
|
+
key: string;
|
|
262
|
+
holder_job_id: string;
|
|
263
|
+
acquired_at: number;
|
|
264
|
+
}
|
|
265
|
+
export type NormalizedKind = 'session_start' | 'phase' | 'command_start' | 'command_end' | 'tool_error' | 'environment_block' | 'agent_message' | 'final_response' | 'system' | 'raw';
|
|
266
|
+
export type NormalizedSeverity = 'info' | 'warning' | 'error';
|
|
267
|
+
/**
|
|
268
|
+
* One human-readable line derived from one or more raw events. Repeated token
|
|
269
|
+
* deltas, blank lines and duplicates collapse away before this exists.
|
|
270
|
+
*/
|
|
271
|
+
export interface NormalizedEvent {
|
|
272
|
+
kind: NormalizedKind;
|
|
273
|
+
severity: NormalizedSeverity;
|
|
274
|
+
/** `step_index` when the source was a `step_update`, else null. */
|
|
275
|
+
step_idx: number | null;
|
|
276
|
+
/** Single-line, already truncated, safe to concatenate into a tail. */
|
|
277
|
+
text: string;
|
|
278
|
+
/** Present for tool events. */
|
|
279
|
+
tool?: string;
|
|
280
|
+
/** `run_command`'s `CommandLine`, when that is what happened. */
|
|
281
|
+
command?: string;
|
|
282
|
+
/** Wall time agy attributed to the step. */
|
|
283
|
+
duration_seconds?: number;
|
|
284
|
+
/** Byte offset of the source line in `events.ndjson`, when known. */
|
|
285
|
+
offset?: number;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Class 1 — a structured refusal. Detected by
|
|
289
|
+
* `step_type === 'tool' && state === 'ERROR'`; details in `tool_info.error.message`.
|
|
290
|
+
* Our own gate authors that message, so `required_rule` round-trips losslessly.
|
|
291
|
+
*/
|
|
292
|
+
export interface DenialClass1 {
|
|
293
|
+
class: 1;
|
|
294
|
+
tool: string;
|
|
295
|
+
command: string | null;
|
|
296
|
+
/** Rule string the caller can paste straight into the next `permissions.allow`. */
|
|
297
|
+
required_rule: string | null;
|
|
298
|
+
/**
|
|
299
|
+
* Which stage of the gate's decision order refused, when our own gate did.
|
|
300
|
+
* Null for a refusal we did not author.
|
|
301
|
+
*
|
|
302
|
+
* The recovery differs by stage, so the caller needs it: `profile_allowlist`
|
|
303
|
+
* and `default` are fixed by adding `required_rule` to `permissions.allow`,
|
|
304
|
+
* while `containment` and `deny_list` cannot be — the first needs a different
|
|
305
|
+
* path, the second is not narrowable at all.
|
|
306
|
+
*/
|
|
307
|
+
policy: GateDenialPayload['policy'] | null;
|
|
308
|
+
/** Whether the refusal came from our gate (parsed marker) or elsewhere. */
|
|
309
|
+
source: 'gate' | 'agy' | 'unknown';
|
|
310
|
+
message: string;
|
|
311
|
+
step_idx: number | null;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Class 2 — a silent environment block. No error event, exit 0, status SUCCESS.
|
|
315
|
+
* Only visible as ordinary command output, which is why this is the dangerous one.
|
|
316
|
+
*/
|
|
317
|
+
export interface DenialClass2 {
|
|
318
|
+
class: 2;
|
|
319
|
+
tool: string;
|
|
320
|
+
command: string | null;
|
|
321
|
+
/** Which entry of `ENVIRONMENT_BLOCK_SIGNATURES` matched. */
|
|
322
|
+
signature: string;
|
|
323
|
+
/** Short excerpt of the output around the match. */
|
|
324
|
+
excerpt: string;
|
|
325
|
+
step_idx: number | null;
|
|
326
|
+
}
|
|
327
|
+
export interface DenialScan {
|
|
328
|
+
permission_denials: DenialClass1[];
|
|
329
|
+
environment_blocks: DenialClass2[];
|
|
330
|
+
}
|
|
331
|
+
export type RuleVerb = 'command' | 'read_file' | 'write_file' | 'fetch' | 'url' | 'mcp' | 'browser';
|
|
332
|
+
/** `command(git status)` / `regex:` opt-in, per §12. Strict non-regex by default. */
|
|
333
|
+
export interface ParsedRule {
|
|
334
|
+
verb: RuleVerb;
|
|
335
|
+
pattern: string;
|
|
336
|
+
/** True when the pattern was written as `regex:...`. */
|
|
337
|
+
regex: boolean;
|
|
338
|
+
raw: string;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* What a client may ask for on `agy_start`. Narrowing only — never widening.
|
|
342
|
+
* `sandboxed: true` forces agy's OS sandbox on for this job even on general_worker.
|
|
343
|
+
* `read_roots` narrows the *human ceiling* (`policy/ceiling.ts`).
|
|
344
|
+
*/
|
|
345
|
+
/** OS boundary for an allowed `run_command` (0.3.0 PR6). Ordered: none < seatbelt < agy. */
|
|
346
|
+
export type SandboxMode = 'none' | 'seatbelt' | 'agy';
|
|
347
|
+
export interface RequestedPermissions {
|
|
348
|
+
allow?: string[];
|
|
349
|
+
deny?: string[];
|
|
350
|
+
/** Forces agy's OS sandbox on for this job (0.2.0 behaviour). Narrowing only: true forces sandbox on; false or absent changes nothing. Same as `sandbox: 'agy'`. */
|
|
351
|
+
sandboxed?: boolean;
|
|
352
|
+
/** Tightening only: `seatbelt` or `agy`. Never loosens what the profile or ceiling set. */
|
|
353
|
+
sandbox?: 'seatbelt' | 'agy';
|
|
354
|
+
/** Paths to add as extra read/exec roots (`--add-dir`, M4). Each must match a ceiling glob. */
|
|
355
|
+
read_roots?: string[];
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The resolved policy written to `jobs/<id>/policy.json` and read by the gate on
|
|
359
|
+
* every tool call. `allow` is already intersected with the profile ceiling and
|
|
360
|
+
* `deny` already unions the profile's list with `HARD_DENY`.
|
|
361
|
+
*
|
|
362
|
+
* `network` and `default_decision` are gone: a bound job's gate never
|
|
363
|
+
* answers `ask` (I1), so there is no verdict left for "nothing matched" to
|
|
364
|
+
* fall back to — it is simply `deny`.
|
|
365
|
+
* In 0.2.1, allowed commands run without agy's OS sandbox by default on general_worker.
|
|
366
|
+
* `bypass_sandbox` determines `overwrite.BypassSandbox` for allowed run_command calls.
|
|
367
|
+
*/
|
|
368
|
+
export interface EffectivePolicy {
|
|
369
|
+
profile: Profile;
|
|
370
|
+
/** Canonical workspace. The gate forces `overwrite.Cwd` to this. */
|
|
371
|
+
workspace: string;
|
|
372
|
+
read_roots: string[];
|
|
373
|
+
write_roots: string[];
|
|
374
|
+
allow: string[];
|
|
375
|
+
deny: string[];
|
|
376
|
+
/**
|
|
377
|
+
* Which OS boundary an allowed `run_command` runs under (0.3.0 PR6):
|
|
378
|
+
* `none` (as the user, the 0.2.1 default), `seatbelt` (our own
|
|
379
|
+
* `sandbox-exec` profile: writes only inside `seatbelt_write_roots`), or
|
|
380
|
+
* `agy` (agy's sandbox, `BypassSandbox: false`; always on research_readonly).
|
|
381
|
+
* The strictest of profile / ceiling / request wins.
|
|
382
|
+
*/
|
|
383
|
+
sandbox: SandboxMode;
|
|
384
|
+
/** Who set `sandbox`: 'default' when nothing asked for more than none. */
|
|
385
|
+
sandbox_source: 'profile' | 'ceiling' | 'request' | 'default';
|
|
386
|
+
/** Directories the seatbelt profile lets commands write to: `write_roots` plus tmp/dev. */
|
|
387
|
+
seatbelt_write_roots: string[];
|
|
388
|
+
/**
|
|
389
|
+
* Whether allowed run_command calls emit `overwrite.BypassSandbox: true` (I2).
|
|
390
|
+
* Derived: `sandbox !== 'agy'`. Kept as its own field because the gate,
|
|
391
|
+
* broker and every job policy file read it.
|
|
392
|
+
*/
|
|
393
|
+
bypass_sandbox: boolean;
|
|
394
|
+
/** Records where bypass_sandbox: false came from, or null when bypassing. Derived from `sandbox_source`. */
|
|
395
|
+
sandbox_forced_by: 'profile' | 'ceiling' | 'request' | null;
|
|
396
|
+
/**
|
|
397
|
+
* Extra read-only roots beyond the workspace (`--add-dir`, M4). `[]` unless
|
|
398
|
+
* `requested.read_roots` is given *and* each entry matches a glob in
|
|
399
|
+
* the human ceiling's own `read_roots` (`policy/ceiling.ts`) — a
|
|
400
|
+
* non-matching entry is dropped and reported in `rejected_read_roots`,
|
|
401
|
+
* never silently widened.
|
|
402
|
+
*/
|
|
403
|
+
add_dirs: string[];
|
|
404
|
+
/** Where add_dirs came from: 'ceiling' (default applied), 'request' (narrowed subset), or 'none'. */
|
|
405
|
+
add_dirs_source: 'ceiling' | 'request' | 'none';
|
|
406
|
+
/** Whether commands default to allow when no allow rule matches (0.2.2 PR3). */
|
|
407
|
+
command_policy: 'allowlist' | 'denylist';
|
|
408
|
+
/** Shape version of this file. */
|
|
409
|
+
policy_version: 3;
|
|
410
|
+
on_denial: OnDenial;
|
|
411
|
+
/**
|
|
412
|
+
* Abort the job after this many gate denials regardless of on_denial. Null = never.
|
|
413
|
+
* A middle ground between `abort` (one flaky-tool workaround kills the job) and
|
|
414
|
+
* `continue` (unbounded denial loops).
|
|
415
|
+
*/
|
|
416
|
+
max_denials: number | null;
|
|
417
|
+
/** Allow entries the client asked for that the profile ceiling refused. */
|
|
418
|
+
rejected_allow: string[];
|
|
419
|
+
/** Profile deny rules the ceiling's `exceptions` lifted for this job (0.3.0), `{workspace}`-substituted. */
|
|
420
|
+
lifted: string[];
|
|
421
|
+
/** `requested.read_roots` entries that matched no glob in the human ceiling's `read_roots`. */
|
|
422
|
+
rejected_read_roots: string[];
|
|
423
|
+
/** Whether a project ceiling file existed when this policy was resolved (0.3.0). */
|
|
424
|
+
ceiling_present: boolean;
|
|
425
|
+
/** Where that file is (or would be); null when unknown (pre-0.3.0 job files). */
|
|
426
|
+
ceiling_path: string | null;
|
|
427
|
+
/** Pre-flight warnings produced during policy resolution. */
|
|
428
|
+
warnings?: string[];
|
|
429
|
+
}
|
|
430
|
+
/** Exactly the JSON agy writes to the hook's stdin. */
|
|
431
|
+
/**
|
|
432
|
+
* What `agy_start` tells the caller about the policy it just resolved.
|
|
433
|
+
*
|
|
434
|
+
* `agy_start`'s non-dry_run reply used to carry no policy information at all,
|
|
435
|
+
* so a caller whose `permissions.allow` was rejected wholesale could not tell
|
|
436
|
+
* (observed: a request of three build commands collapsed `allow` to `[]`,
|
|
437
|
+
* silently dropping the profile's own defaults too). Both replies carry this
|
|
438
|
+
* now, alongside `warnings` built by the same function.
|
|
439
|
+
*/
|
|
440
|
+
export interface PolicySummary {
|
|
441
|
+
profile: Profile;
|
|
442
|
+
/**
|
|
443
|
+
* Size of the effective `allow` list. `0` means nothing is explicitly
|
|
444
|
+
* allowed — including the profile's own defaults, which a rejected
|
|
445
|
+
* `permissions.allow` request silently takes with it. Rejected entries are
|
|
446
|
+
* reported as `source: 'policy_ceiling'` blockers, not here.
|
|
447
|
+
*/
|
|
448
|
+
allow_count: number;
|
|
449
|
+
/** Whether allowed commands run unsandboxed for this job. */
|
|
450
|
+
bypass_sandbox: boolean;
|
|
451
|
+
/** Why the sandbox is forced on, or null when bypassing. */
|
|
452
|
+
sandbox_forced_by: 'profile' | 'ceiling' | 'request' | null;
|
|
453
|
+
/** `EffectivePolicy.sandbox` / `sandbox_source`. */
|
|
454
|
+
sandbox: SandboxMode;
|
|
455
|
+
sandbox_source: 'profile' | 'ceiling' | 'request' | 'default';
|
|
456
|
+
/** `EffectivePolicy.add_dirs`, verbatim. */
|
|
457
|
+
add_dirs: string[];
|
|
458
|
+
/** Where add_dirs came from. */
|
|
459
|
+
add_dirs_source: 'ceiling' | 'request' | 'none';
|
|
460
|
+
}
|
|
461
|
+
export interface GatePayload {
|
|
462
|
+
conversationId: string;
|
|
463
|
+
stepIdx?: number;
|
|
464
|
+
modelName?: string;
|
|
465
|
+
toolCall: {
|
|
466
|
+
name: string;
|
|
467
|
+
args?: Record<string, unknown>;
|
|
468
|
+
};
|
|
469
|
+
workspacePaths?: string[];
|
|
470
|
+
transcriptPath?: string;
|
|
471
|
+
artifactDirectoryPath?: string;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Documented values are `allow` / `deny` / `ask` / `force_ask`.
|
|
475
|
+
*
|
|
476
|
+
* `ask` is the pass-through: it delegates to agy's built-in engine, which
|
|
477
|
+
* auto-approves under `proceed-in-sandbox`.
|
|
478
|
+
*/
|
|
479
|
+
export type GateVerdict = 'allow' | 'deny' | 'ask' | 'force_ask';
|
|
480
|
+
/**
|
|
481
|
+
* What the gate writes to stdout.
|
|
482
|
+
*
|
|
483
|
+
* ⚠ `{}` IS A DENIAL (measured, §9). The gate must emit a `decision` on every
|
|
484
|
+
* path including parse failure, DB failure and unhandled exception — otherwise it
|
|
485
|
+
* breaks the user's own interactive agy sessions, the one place this package can
|
|
486
|
+
* affect anything outside its own jobs.
|
|
487
|
+
*/
|
|
488
|
+
export interface GateDecision {
|
|
489
|
+
decision: GateVerdict;
|
|
490
|
+
/** Passed to the model verbatim; used as an instruction channel, not just a reason. */
|
|
491
|
+
reason?: string;
|
|
492
|
+
/** e.g. `["command(npm test)"]`. Scope of persistence is unmeasured — see §12. */
|
|
493
|
+
permissionOverrides?: string[];
|
|
494
|
+
/** Shallow-merged into the tool args. We use it to pin `Cwd`. */
|
|
495
|
+
overwrite?: Record<string, unknown>;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Which stage of `decide()`'s fixed order produced a verdict.
|
|
499
|
+
* `bound_passthrough`/`network` are gone with `ask` and
|
|
500
|
+
* `default_decision` (I1: a bound job's gate never answers `ask`). `unsupported`
|
|
501
|
+
* and `control` are new — every tool call is now classified, not just
|
|
502
|
+
* `run_command`.
|
|
503
|
+
*/
|
|
504
|
+
export type GateDecisionStage = 'unsupported' | 'control' | 'containment' | 'deny_list' | 'profile_allowlist' | 'default' | 'denylist_default';
|
|
505
|
+
/** Machine payload embedded after {@link GATE_DENIAL_MARKER} inside `reason`. */
|
|
506
|
+
export interface GateDenialPayload {
|
|
507
|
+
job_id: string;
|
|
508
|
+
tool: string;
|
|
509
|
+
required_rule: string | null;
|
|
510
|
+
policy: GateDecisionStage;
|
|
511
|
+
on_denial: OnDenial;
|
|
512
|
+
}
|
|
513
|
+
/** Row appended to `jobs/<id>/gate-log.jsonl` — every verdict, allow included. */
|
|
514
|
+
export interface GateLogEntry {
|
|
515
|
+
ts: number;
|
|
516
|
+
job_id: string;
|
|
517
|
+
conversation_id: string;
|
|
518
|
+
step_idx: number | null;
|
|
519
|
+
tool: string;
|
|
520
|
+
command: string | null;
|
|
521
|
+
decision: GateVerdict;
|
|
522
|
+
policy: GateDecisionStage;
|
|
523
|
+
matched_rule: string | null;
|
|
524
|
+
reason: string | null;
|
|
525
|
+
abort_reason?: 'on_denial' | 'max_denials';
|
|
526
|
+
}
|
|
527
|
+
/** Verbatim `agy_start` input, persisted to `jobs/<id>/request.json`. */
|
|
528
|
+
export interface JobRequest {
|
|
529
|
+
prompt: string;
|
|
530
|
+
cwd?: string;
|
|
531
|
+
profile?: Profile;
|
|
532
|
+
model?: string;
|
|
533
|
+
effort?: string;
|
|
534
|
+
mode?: string;
|
|
535
|
+
session_id?: string;
|
|
536
|
+
session_mode?: SessionMode;
|
|
537
|
+
permissions?: RequestedPermissions;
|
|
538
|
+
on_denial?: OnDenial;
|
|
539
|
+
/**
|
|
540
|
+
* Abort the job after this many gate denials regardless of on_denial. Null = never.
|
|
541
|
+
* A middle ground between `abort` (one flaky-tool workaround kills the job) and
|
|
542
|
+
* `continue` (unbounded denial loops).
|
|
543
|
+
*/
|
|
544
|
+
max_denials?: number;
|
|
545
|
+
timeout_ms?: number;
|
|
546
|
+
/**
|
|
547
|
+
* `session_mode: 'session'` only. Closes stdin (ending the process at EOF,
|
|
548
|
+
* §6) after this many idle ms following a *completed* turn with no new
|
|
549
|
+
* `agy_send` in between. Ignored for `oneshot`, which already closes stdin
|
|
550
|
+
* after its one turn.
|
|
551
|
+
*
|
|
552
|
+
* Deliberately separate from `timeout_ms`/`deadline_at`: `agy_send` never
|
|
553
|
+
* extends `deadline_at` (see the comment on `EffectiveConfig.deadline_at`
|
|
554
|
+
* below), so this is the mechanism that lets a multi-turn session end
|
|
555
|
+
* promptly instead of idling out the full hard timeout (a session-mode job
|
|
556
|
+
* that finished its turn would otherwise sit until the hard deadline elapsed).
|
|
557
|
+
*/
|
|
558
|
+
idle_timeout_ms?: number;
|
|
559
|
+
/** Workspace-relative paths that must exist when the job finishes. */
|
|
560
|
+
expected_artifacts?: string[];
|
|
561
|
+
/** Path or inline JSON schema for `--json-schema`. */
|
|
562
|
+
json_schema?: string;
|
|
563
|
+
requested_by?: string;
|
|
564
|
+
parent_task_id?: string;
|
|
565
|
+
dry_run?: boolean;
|
|
566
|
+
/** `dry_run` only: shell commands to judge against the effective policy up front (0.2.2 PR5). */
|
|
567
|
+
expected_commands?: string[];
|
|
568
|
+
/**
|
|
569
|
+
* A command the *runner* runs once, after agy exits normally, against the
|
|
570
|
+
* final workspace state (PR6, see docs/permissions.md). Outside
|
|
571
|
+
* the model's own decisions — it cannot skip, reorder or narrow it — which is
|
|
572
|
+
* the whole reason it exists, not sandboxing: it runs as the user, with no
|
|
573
|
+
* sandbox, at the same trust level as the parent agent running the command
|
|
574
|
+
* itself. A failing verify is a job *failure* (`outcome: 'failed'`), never a
|
|
575
|
+
* `Blocker` — that vocabulary means "who refused", and nothing refused here.
|
|
576
|
+
*/
|
|
577
|
+
verify_command?: string;
|
|
578
|
+
/** Default 600000ms, clamped to `limits.max_timeout_ms`. Ignored without `verify_command`. */
|
|
579
|
+
verify_timeout_ms?: number;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Everything resolved before spawning, persisted to
|
|
583
|
+
* `jobs/<id>/effective-config.json`. `agy_start(dry_run: true)` returns this
|
|
584
|
+
* without spawning so a caller can settle configuration without burning quota.
|
|
585
|
+
*/
|
|
586
|
+
export interface EffectiveConfig {
|
|
587
|
+
job_id: string;
|
|
588
|
+
session_id: string;
|
|
589
|
+
/** Set only when resuming an existing conversation. */
|
|
590
|
+
conversation_id: string | null;
|
|
591
|
+
/** Canonical, symlink-resolved workspace. Also the `--add-dir` value. */
|
|
592
|
+
cwd: string;
|
|
593
|
+
profile: Profile;
|
|
594
|
+
model: string | null;
|
|
595
|
+
effort: string | null;
|
|
596
|
+
mode: string | null;
|
|
597
|
+
session_mode: SessionMode;
|
|
598
|
+
on_denial: OnDenial;
|
|
599
|
+
write_mode: boolean;
|
|
600
|
+
timeout_ms: number;
|
|
601
|
+
/**
|
|
602
|
+
* Hard ceiling, set once from `timeout_ms` at `agy_start` and never touched
|
|
603
|
+
* again — `agy_send` does not extend it.
|
|
604
|
+
*
|
|
605
|
+
* A queued turn keeps the *original* deadline. Pushing it out on
|
|
606
|
+
* every `agy_send` would let an actively-fed session hold its `session` and
|
|
607
|
+
* `cwd_write` locks indefinitely as long as something kept
|
|
608
|
+
* calling `agy_send` faster than it decayed — and because `reconcile` only
|
|
609
|
+
* runs from a tool entry point, nothing else would ever notice and
|
|
610
|
+
* cut it off. A fixed ceiling is the backstop that guarantees a `session`
|
|
611
|
+
* job eventually reconciles even if a client behaves badly.
|
|
612
|
+
*
|
|
613
|
+
* The intended way to run longer than one `deadline_at` window is not to
|
|
614
|
+
* stretch it, but to let the job finish (or idle-close, see
|
|
615
|
+
* `idle_timeout_ms`) and resume the same conversation with a fresh
|
|
616
|
+
* `agy_start(session_id=...)` — conversation resume is lossless,
|
|
617
|
+
* so nothing is lost by not stretching this field.
|
|
618
|
+
*/
|
|
619
|
+
deadline_at: number;
|
|
620
|
+
/**
|
|
621
|
+
* Resolved `idle_timeout_ms`, or `null` when `session_mode !== 'session'`
|
|
622
|
+
* (a oneshot job has no idle window — its one turn's `result` is followed
|
|
623
|
+
* immediately by stdin close). See `JobRequest.idle_timeout_ms`.
|
|
624
|
+
*/
|
|
625
|
+
idle_timeout_ms: number | null;
|
|
626
|
+
expected_artifacts: string[];
|
|
627
|
+
json_schema_path: string | null;
|
|
628
|
+
policy: EffectivePolicy;
|
|
629
|
+
/**
|
|
630
|
+
* Every `--add-dir` value actually passed to `buildAgyArgv` — `[cwd,
|
|
631
|
+
* ...policy.add_dirs]`. Kept
|
|
632
|
+
* distinct from `policy.add_dirs` (which is only the *extra* roots
|
|
633
|
+
* beyond the workspace) so this field is a direct, typed record of what the
|
|
634
|
+
* argv actually contains, without a reader having to reconstruct it from
|
|
635
|
+
* `cwd` + `policy`.
|
|
636
|
+
*/
|
|
637
|
+
add_dirs: string[];
|
|
638
|
+
/**
|
|
639
|
+
* Resolved `verify_command`/`verify_timeout_ms`, or `null` when the job asked
|
|
640
|
+
* for no verify command at all. `command` is validated at `agy_start` the
|
|
641
|
+
* same way any other `command(...)` rule subject is — see `handleStart`'s
|
|
642
|
+
* `firstMatchForDenial` check against the resolved `policy.deny` — but is
|
|
643
|
+
* otherwise opaque here: the runner passes it to `sh -c` verbatim.
|
|
644
|
+
*/
|
|
645
|
+
verify: {
|
|
646
|
+
command: string;
|
|
647
|
+
timeout_ms: number;
|
|
648
|
+
} | null;
|
|
649
|
+
/** Exact argv handed to `spawn`. No shell string exists anywhere. */
|
|
650
|
+
argv: string[];
|
|
651
|
+
/** Absolute path of the executable being spawned. */
|
|
652
|
+
agy_bin: string;
|
|
653
|
+
/** Allowlisted environment passed to the child. */
|
|
654
|
+
env: Record<string, string>;
|
|
655
|
+
created_at: number;
|
|
656
|
+
}
|
|
657
|
+
/** `jobs/<id>/state.json` — written atomically by the runner. */
|
|
658
|
+
export interface JobStateFile {
|
|
659
|
+
job_id: string;
|
|
660
|
+
lifecycle: Lifecycle;
|
|
661
|
+
pid: number | null;
|
|
662
|
+
pgid: number | null;
|
|
663
|
+
proc_start_time: string | null;
|
|
664
|
+
started_at: number | null;
|
|
665
|
+
finished_at: number | null;
|
|
666
|
+
updated_at: number;
|
|
667
|
+
/**
|
|
668
|
+
* True when the runner's own deadline watchdog killed the process group.
|
|
669
|
+
*
|
|
670
|
+
* Without it the fact is lost: the runner kills agy, writes the resulting
|
|
671
|
+
* `exit_code` (1, or a signal code), and `reconcile` — which only sees the
|
|
672
|
+
* exit code — classifies a timeout as an ordinary `failed`. Measured live:
|
|
673
|
+
* `timeout_ms: 25000` produced `outcome: "failed", exit_code: 1`.
|
|
674
|
+
*
|
|
675
|
+
* Optional because a `state.json` written by an older build will not have it.
|
|
676
|
+
*/
|
|
677
|
+
timed_out?: boolean;
|
|
678
|
+
/**
|
|
679
|
+
* True when the runner's idle watchdog closed stdin because
|
|
680
|
+
* `idle_timeout_ms` elapsed with no `agy_send` after the last completed
|
|
681
|
+
* turn. Distinct from `timed_out`: this is a clean
|
|
682
|
+
* EOF close, not a `killpg` — the exit code it produces goes through the
|
|
683
|
+
* ordinary `finalizeJob` path exactly as an explicit `agy_send(close:true)`
|
|
684
|
+
* would, so it never forces `outcome`. Diagnostic only, so a caller can tell
|
|
685
|
+
* "the session idled itself shut" apart from "the client closed it" or "the
|
|
686
|
+
* process just exited on its own" after reading the exit code.
|
|
687
|
+
*
|
|
688
|
+
* Optional for the same reason as `timed_out`: an older `state.json` won't
|
|
689
|
+
* have it, and jobs that never had an idle watchdog (oneshot, or a
|
|
690
|
+
* `session` job the client closed itself) never set it either.
|
|
691
|
+
*/
|
|
692
|
+
idle_closed?: boolean;
|
|
693
|
+
/**
|
|
694
|
+
* I4: whether the runner's own gate
|
|
695
|
+
* watchdog (`src/runner/gate-watchdog.ts`) saw `jobs/<id>/gate-log.jsonl`
|
|
696
|
+
* receive a line by the time the first `step_type: 'tool'` step reached
|
|
697
|
+
* DONE/ERROR (plus a short grace period), or within a long backstop cap if
|
|
698
|
+
* that first step never finished.
|
|
699
|
+
*
|
|
700
|
+
* - `true` — the gate confirmed itself; nothing else changes.
|
|
701
|
+
* - `false` — no gate-log line appeared even though a tool call completed
|
|
702
|
+
* (a PreToolUse hook gates execution, so a loaded gate must have logged
|
|
703
|
+
* before then), so the watchdog killed the process group:
|
|
704
|
+
* `hooks.json` never actually loaded and every
|
|
705
|
+
* tool call up to that point ran completely unguarded. `reconcile` forces
|
|
706
|
+
* `outcome: 'process_error'` for this, never `blocked` — a permission
|
|
707
|
+
* verdict implies the gate ran at all, which is exactly what did not
|
|
708
|
+
* happen here.
|
|
709
|
+
* - `null` (or absent, for a `state.json` written before this field
|
|
710
|
+
* existed) — the job ended before any tool step ever ran, so there was
|
|
711
|
+
* nothing for the gate to confirm; this never forces an outcome.
|
|
712
|
+
*/
|
|
713
|
+
gate_confirmed?: boolean | null;
|
|
714
|
+
/**
|
|
715
|
+
* True once the runner has written `jobs/<id>/verify.json` (PR6 §6.2).
|
|
716
|
+
* `false` or absent otherwise — including when `config.verify` is null (no
|
|
717
|
+
* verify command was requested) or the deadline killed agy first
|
|
718
|
+
* (`timedOut === true` skips verify entirely; `verify_done` stays falsy and
|
|
719
|
+
* `verification.verify` reads `null`, exactly like "no verify configured").
|
|
720
|
+
*/
|
|
721
|
+
verify_done?: boolean;
|
|
722
|
+
/**
|
|
723
|
+
* Which process the runner is currently waiting on (0.2.2 PR1).
|
|
724
|
+
*
|
|
725
|
+
* - `'agy'` — the agy child is (or was just) running.
|
|
726
|
+
* - `'verifying'` — agy has exited and the runner is running `verify_command`.
|
|
727
|
+
* agy's pid is gone in this phase and `exit_code` is not yet written, so a
|
|
728
|
+
* reconciler that reads only those two facts sees exactly what a lost
|
|
729
|
+
* runner looks like. Measured 2026-09-08 (audit `08`): every job that ran a
|
|
730
|
+
* multi-second verify (gradle build, `npm test`) came back `process_error`
|
|
731
|
+
* with a complete result landing a few seconds later.
|
|
732
|
+
* - `'done'` — the final write; `exit_code` follows immediately.
|
|
733
|
+
*
|
|
734
|
+
* Optional because a `state.json` written by an older build will not have it.
|
|
735
|
+
*/
|
|
736
|
+
phase?: 'agy' | 'verifying' | 'done';
|
|
737
|
+
/**
|
|
738
|
+
* The runner's own pid, as opposed to `pid` (agy's). Lets `reconcile` tell
|
|
739
|
+
* "agy exited and the runner is still working" (`verifying`, or the short
|
|
740
|
+
* writeback window) apart from "the runner itself is gone". Optional for the
|
|
741
|
+
* same reason as `phase`.
|
|
742
|
+
*/
|
|
743
|
+
runner_pid?: number;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* A queued turn. Byte-identical to agy's stream-json input schema (§5) so the
|
|
747
|
+
* runner can relay the line to stdin without re-encoding it.
|
|
748
|
+
*/
|
|
749
|
+
export type InboxUserLine = AgyStreamUserInput;
|
|
750
|
+
/**
|
|
751
|
+
* Control line the runner consumes and never relays. Uses a key agy would ignore
|
|
752
|
+
* anyway, but the runner filters it out before stdin so that never matters.
|
|
753
|
+
*/
|
|
754
|
+
export interface InboxControlLine {
|
|
755
|
+
agy_worker_control: 'close';
|
|
756
|
+
ts: number;
|
|
757
|
+
}
|
|
758
|
+
export type InboxLine = InboxUserLine | InboxControlLine;
|
|
759
|
+
export interface ArtifactCheck {
|
|
760
|
+
path: string;
|
|
761
|
+
absolute: string;
|
|
762
|
+
exists: boolean;
|
|
763
|
+
size: number | null;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Written verbatim to `jobs/<id>/verify.json` by the runner (PR6 §6.2), once,
|
|
767
|
+
* right after `verify_command` exits or its own `verify_timeout_ms` fires.
|
|
768
|
+
* Never written at all when no `verify_command` was requested, or when the
|
|
769
|
+
* job's own deadline killed agy first (`timedOut === true` skips verify).
|
|
770
|
+
*/
|
|
771
|
+
export interface VerifyRecord {
|
|
772
|
+
command: string;
|
|
773
|
+
exit_code: number | null;
|
|
774
|
+
/** Node's own signal name (e.g. `'SIGTERM'`), or null on an ordinary exit. */
|
|
775
|
+
signal: string | null;
|
|
776
|
+
started_at: number;
|
|
777
|
+
duration_ms: number;
|
|
778
|
+
/** True when `verify_timeout_ms` fired and the runner killed the process group. */
|
|
779
|
+
timed_out: boolean;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* `Verification.verify` — {@link VerifyRecord} plus a display-sized excerpt of
|
|
783
|
+
* its own log, read fresh from disk by `verifyJob` rather than carried through
|
|
784
|
+
* the runner. `output_tail` is the last 2 KiB of `jobs/<id>/verify.log`, or
|
|
785
|
+
* `''` when the file is empty or missing.
|
|
786
|
+
*/
|
|
787
|
+
export interface VerifyResult extends VerifyRecord {
|
|
788
|
+
output_tail: string;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Where a refusal came from. The one axis that decides what a caller should do
|
|
792
|
+
* next, so every judgement surface (outcome, counts, warnings, `agy_start`'s own
|
|
793
|
+
* reply) is derived from this and nothing else. The authoritative
|
|
794
|
+
* source → (`actionable`, `remedy`, `blocks_outcome`) table lives in
|
|
795
|
+
* `src/broker/blockers.ts`; do not re-derive it anywhere.
|
|
796
|
+
*/
|
|
797
|
+
export type BlockerSource =
|
|
798
|
+
/** A requested `permissions.allow` entry the profile ceiling refused. Pre-flight, from `resolvePolicy`. */
|
|
799
|
+
'policy_ceiling'
|
|
800
|
+
/** Our own PreToolUse gate refused the call. The only refusal we can confirm. */
|
|
801
|
+
| 'gate'
|
|
802
|
+
/** agy's own permission engine refused, outside our policy entirely (measured wording). */
|
|
803
|
+
| 'agy_engine'
|
|
804
|
+
/** agy's sandbox blocked it silently — a Class 2 signature match. */
|
|
805
|
+
| 'sandbox'
|
|
806
|
+
/** A broker-side check failed: a missing `expected_artifacts` entry. */
|
|
807
|
+
| 'broker'
|
|
808
|
+
/**
|
|
809
|
+
* An ordinary failing tool call carrying no refusal signature at all.
|
|
810
|
+
*
|
|
811
|
+
* Not one of the five refusal sources — it exists because a non-gate
|
|
812
|
+
* `state: 'ERROR'` step is indistinguishable from a real denial by shape
|
|
813
|
+
* (finding 17), and dropping those events would lose their message entirely.
|
|
814
|
+
* Never actionable through permissions, never `blocks_outcome`.
|
|
815
|
+
*/
|
|
816
|
+
| 'tool_error';
|
|
817
|
+
/**
|
|
818
|
+
* One thing that stood between the job and `verified_success` — or, for
|
|
819
|
+
* `policy_ceiling`, between the request and the job it asked for.
|
|
820
|
+
*
|
|
821
|
+
* This is deliberately the same vocabulary as an error envelope
|
|
822
|
+
* (`detail` + `remedy`, `contract/errors.ts`): a caller reads `actionable` to
|
|
823
|
+
* decide whether retrying differently can possibly help, and `remedy` for what
|
|
824
|
+
* to change. When nothing can help, `remedy` is null and `message` says why.
|
|
825
|
+
*/
|
|
826
|
+
export interface Blocker {
|
|
827
|
+
source: BlockerSource;
|
|
828
|
+
/** Whether changing the next `agy_start` can lift this. */
|
|
829
|
+
actionable: boolean;
|
|
830
|
+
/** What to change. Null when nothing will help — the reason is in `message`. */
|
|
831
|
+
remedy: string | null;
|
|
832
|
+
/**
|
|
833
|
+
* Whether this forces `outcome: 'blocked'`. True only for refusals we
|
|
834
|
+
* confirmed ourselves (`gate`, `sandbox`, `broker`); an `agy_engine` or
|
|
835
|
+
* `tool_error` entry cannot be told apart from an ordinary command failure,
|
|
836
|
+
* and reporting those as `blocked` would call every failing test a block.
|
|
837
|
+
*/
|
|
838
|
+
blocks_outcome: boolean;
|
|
839
|
+
tool: string | null;
|
|
840
|
+
command: string | null;
|
|
841
|
+
/** Human-readable, and carrying the measured message verbatim where there is one. */
|
|
842
|
+
message: string;
|
|
843
|
+
/**
|
|
844
|
+
* The full original record this was derived from — a `DenialClass1`,
|
|
845
|
+
* `DenialClass2`, `ArtifactCheck`, or the rejected rule string. Nothing the
|
|
846
|
+
* pre-0.1.1 `permission_denials` / `environment_blocks` lists carried
|
|
847
|
+
* (`required_rule`, `signature`, `policy`, `step_idx`, …) is dropped.
|
|
848
|
+
*/
|
|
849
|
+
detail?: Record<string, unknown>;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Written to `jobs/<id>/verification.json`. Facts only.
|
|
853
|
+
*
|
|
854
|
+
* `blockers` replaced the separate `permission_denials` / `environment_blocks`
|
|
855
|
+
* lists in `BROKER_RESULT_VERSION` 2: the caller's real question is "who
|
|
856
|
+
* refused, and can I fix it", which the split lists made them reassemble by
|
|
857
|
+
* hand — and got wrong, since a non-gate tool error sat in a list named
|
|
858
|
+
* `permission_denials` while never counting as a denial anywhere else.
|
|
859
|
+
*/
|
|
860
|
+
export interface Verification {
|
|
861
|
+
blockers: Blocker[];
|
|
862
|
+
expected_artifacts: ArtifactCheck[];
|
|
863
|
+
/** `git status --porcelain` inside the workspace, when it is a repo. */
|
|
864
|
+
changed_files: string[];
|
|
865
|
+
/** Rendered `blockers`, plus non-blocker observations (an idle-closed session). */
|
|
866
|
+
warnings: string[];
|
|
867
|
+
contract_status: ContractStatus;
|
|
868
|
+
checked_at: number;
|
|
869
|
+
/**
|
|
870
|
+
* `null` when no `verify_command` was requested, or the deadline killed agy
|
|
871
|
+
* before it could run. Never a `Blocker` — a failing verify is a job
|
|
872
|
+
* *failure*, not a refusal, and `hasOutcomeBlocker` must never see it
|
|
873
|
+
* (`decideOutcome` reads this field directly instead — see `outcome.ts`).
|
|
874
|
+
*/
|
|
875
|
+
verify: VerifyResult | null;
|
|
876
|
+
}
|
|
877
|
+
/** agy's own claims. Kept quarantined from the broker's findings on purpose. */
|
|
878
|
+
export interface AgentReport {
|
|
879
|
+
status: AgentStatus;
|
|
880
|
+
response: string | null;
|
|
881
|
+
error: string | null;
|
|
882
|
+
num_turns: number | null;
|
|
883
|
+
usage: AgyUsage | null;
|
|
884
|
+
conversation_id: string | null;
|
|
885
|
+
}
|
|
886
|
+
/** Deterministically derived from events, exit status and filesystem checks. */
|
|
887
|
+
export interface BrokerSummary {
|
|
888
|
+
/** One sentence. This is what a caller reads first. */
|
|
889
|
+
headline: string;
|
|
890
|
+
outcome: Outcome;
|
|
891
|
+
exit_code: number | null;
|
|
892
|
+
duration_ms: number | null;
|
|
893
|
+
counts: {
|
|
894
|
+
events: number;
|
|
895
|
+
steps: number;
|
|
896
|
+
tool_calls: number;
|
|
897
|
+
tool_errors: number;
|
|
898
|
+
turns: number;
|
|
899
|
+
malformed_lines: number;
|
|
900
|
+
};
|
|
901
|
+
/** Last normalized lines, already size-capped. */
|
|
902
|
+
log_tail: string[];
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* `jobs/<id>/broker-result.json` — the single source of truth.
|
|
906
|
+
* `agy_wait` projects a subset of this; `agy_result` pages all of it. Nothing
|
|
907
|
+
* else recomputes `outcome`.
|
|
908
|
+
*/
|
|
909
|
+
export interface BrokerResult {
|
|
910
|
+
schema_version: number;
|
|
911
|
+
job_id: string;
|
|
912
|
+
session_id: string | null;
|
|
913
|
+
conversation_id: string | null;
|
|
914
|
+
lifecycle: Lifecycle;
|
|
915
|
+
cwd: string;
|
|
916
|
+
profile: Profile;
|
|
917
|
+
session_mode: SessionMode;
|
|
918
|
+
created_at: number;
|
|
919
|
+
started_at: number | null;
|
|
920
|
+
finished_at: number | null;
|
|
921
|
+
/** Unverified self-report. Never read to decide `outcome`. */
|
|
922
|
+
agent_report: AgentReport;
|
|
923
|
+
/** Verified facts. */
|
|
924
|
+
broker_summary: BrokerSummary;
|
|
925
|
+
verification: Verification;
|
|
926
|
+
agent_status: AgentStatus;
|
|
927
|
+
contract_status: ContractStatus;
|
|
928
|
+
/** Raw structured output when `--json-schema` was used; preserved even if invalid. */
|
|
929
|
+
structured_output: unknown;
|
|
930
|
+
finalized_at: number;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* What `agy_wait` returns on completion — the judgement packet summary.
|
|
934
|
+
* Deliberately excludes lists and raw text; those live in `agy_result`.
|
|
935
|
+
*/
|
|
936
|
+
export interface JudgementPacket {
|
|
937
|
+
job_id: string;
|
|
938
|
+
lifecycle: Lifecycle;
|
|
939
|
+
outcome: Outcome | null;
|
|
940
|
+
headline: string;
|
|
941
|
+
exit_code: number | null;
|
|
942
|
+
duration_ms: number | null;
|
|
943
|
+
agent_status: AgentStatus | null;
|
|
944
|
+
contract_status: ContractStatus | null;
|
|
945
|
+
counts: {
|
|
946
|
+
/**
|
|
947
|
+
* Size of `verification.blockers`. The packet stays a verdict, not a
|
|
948
|
+
* report: the per-item detail (source, remedy, verbatim message) is one
|
|
949
|
+
* `agy_result({ section: "verification" })` away.
|
|
950
|
+
*
|
|
951
|
+
* Invariant, for a job that actually ran to a conclusion (i.e. not
|
|
952
|
+
* `canceled` / `timed_out` / `failed`, which outrank verification in
|
|
953
|
+
* `decideOutcome`'s precedence): `outcome === 'blocked'` ⟺ some blocker
|
|
954
|
+
* has `blocks_outcome: true`. Both sides are computed from the same list.
|
|
955
|
+
*/
|
|
956
|
+
blockers: number;
|
|
957
|
+
/** How many of those a different `agy_start` could lift (`actionable: true`). */
|
|
958
|
+
actionable: number;
|
|
959
|
+
/** Every `step_type: 'tool'`, `state: 'ERROR'` step in the raw stream. */
|
|
960
|
+
tool_errors: number;
|
|
961
|
+
/** `result` events, i.e. completed turns. */
|
|
962
|
+
turns: number;
|
|
963
|
+
};
|
|
964
|
+
warnings: string[];
|
|
965
|
+
log_tail: string[];
|
|
966
|
+
/** Byte offset to resume `agy_logs` / `agy_wait` from. */
|
|
967
|
+
cursor: number;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* What the connected MCP client told us at `initialize`.
|
|
971
|
+
*
|
|
972
|
+
* Reported so a caller can see, rather than guess, which optional protocol
|
|
973
|
+
* features are actually negotiated on this connection — `capabilities.tasks`
|
|
974
|
+
* in particular decides whether a long call can be handed to the client as a
|
|
975
|
+
* background task instead of blocking its turn. Null before `initialize`
|
|
976
|
+
* completes, or when the server is driven without a client (tests).
|
|
977
|
+
*/
|
|
978
|
+
export interface ClientSnapshot {
|
|
979
|
+
name: string | null;
|
|
980
|
+
version: string | null;
|
|
981
|
+
/** Verbatim `ClientCapabilities` — an empty object means "declared nothing". */
|
|
982
|
+
capabilities: Record<string, unknown> | null;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* `agy_capabilities.ceiling` — the human-owned permission ceiling as loaded
|
|
986
|
+
* from `<project state dir>/policy.json` (`policy/ceiling.ts`, see
|
|
987
|
+
* `docs/permissions.md`). Rule strings are reported exactly as written in the
|
|
988
|
+
* file: capabilities has no per-job workspace, so a `{workspace}` placeholder
|
|
989
|
+
* inside `allow`/`deny`/`exceptions` is left literal rather than
|
|
990
|
+
* substituted — a caller sees the resolved, job-real form in `agy_start`'s own
|
|
991
|
+
* `policy_summary` instead.
|
|
992
|
+
*/
|
|
993
|
+
export interface CeilingSummary {
|
|
994
|
+
/** Absolute path of the ceiling file, whether or not it exists. */
|
|
995
|
+
path: string;
|
|
996
|
+
present: boolean;
|
|
997
|
+
/** Schema version the file was written in (2 current; 1 is read and converted). Null when absent. */
|
|
998
|
+
version: 1 | 2 | null;
|
|
999
|
+
allow: string[];
|
|
1000
|
+
deny: string[];
|
|
1001
|
+
/** Profile deny rules this project lifts (0.3.0). Exact-string matches only; never touches HARD_DENY. */
|
|
1002
|
+
exceptions: string[];
|
|
1003
|
+
/** OS boundary for general_worker commands in this project: none (default) | seatbelt | agy. */
|
|
1004
|
+
sandbox: SandboxMode;
|
|
1005
|
+
/** `~`-expanded, canonicalized glob patterns, as loaded. Read/exec roots outside the workspace. */
|
|
1006
|
+
read_roots: string[];
|
|
1007
|
+
/** Extra directories jobs may write to (containment and, under seatbelt, the kernel). */
|
|
1008
|
+
write_roots: string[];
|
|
1009
|
+
command_policy: 'allowlist' | 'denylist';
|
|
1010
|
+
/** Non-fatal notes about the file (e.g. a v1 file whose keys should be renamed). */
|
|
1011
|
+
warnings: string[];
|
|
1012
|
+
}
|
|
1013
|
+
/** Risk class `agy_ceiling` attaches to every rule in a draft (`policy/ceiling-review.ts`). */
|
|
1014
|
+
export type CeilingRuleRisk = 'read_utility' | 'build' | 'vcs_local' | 'vcs_remote' | 'network' | 'install' | 'destructive' | 'privilege' | 'filesystem' | 'other';
|
|
1015
|
+
export interface CeilingRuleReview {
|
|
1016
|
+
key: 'allow' | 'deny' | 'exceptions';
|
|
1017
|
+
rule: string;
|
|
1018
|
+
risk: CeilingRuleRisk;
|
|
1019
|
+
/** Advisory notes: redundant, lifts nothing, needs explicit approval. */
|
|
1020
|
+
notes: string[];
|
|
1021
|
+
}
|
|
1022
|
+
/** `agy_ceiling({ draft })` — would the file load, and what is the human being asked to approve. */
|
|
1023
|
+
export interface CeilingDraftReview {
|
|
1024
|
+
/** True when `loadCeiling` would accept this object. */
|
|
1025
|
+
ok: boolean;
|
|
1026
|
+
errors: string[];
|
|
1027
|
+
warnings: string[];
|
|
1028
|
+
rules: CeilingRuleReview[];
|
|
1029
|
+
}
|
|
1030
|
+
/** `agy_ceiling` reply. Read-only: `writes_nothing` is literal. */
|
|
1031
|
+
export interface CeilingReply {
|
|
1032
|
+
path: string;
|
|
1033
|
+
present: boolean;
|
|
1034
|
+
ceiling: CeilingSummary;
|
|
1035
|
+
/** general_worker resolved against the draft (when it loads) or the current file. */
|
|
1036
|
+
effective: {
|
|
1037
|
+
profile: 'general_worker';
|
|
1038
|
+
allow: string[];
|
|
1039
|
+
deny: string[];
|
|
1040
|
+
hard_deny: string[];
|
|
1041
|
+
lifted: string[];
|
|
1042
|
+
read_roots: string[];
|
|
1043
|
+
command_policy: 'allowlist' | 'denylist';
|
|
1044
|
+
warnings: string[];
|
|
1045
|
+
};
|
|
1046
|
+
review: CeilingDraftReview | null;
|
|
1047
|
+
preflight: Array<{
|
|
1048
|
+
command: string;
|
|
1049
|
+
decision: 'allow' | 'deny';
|
|
1050
|
+
stage: string;
|
|
1051
|
+
required_rule: string | null;
|
|
1052
|
+
}> | null;
|
|
1053
|
+
/** Denial history and command frequencies across this project's jobs; null when a draft was given. */
|
|
1054
|
+
history: import('../trace/digest.js').ProjectDigest | null;
|
|
1055
|
+
writes_nothing: true;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* One entry of `Capabilities.models`: the `--effort` values agy accepts for
|
|
1059
|
+
* this model (measured, M8). Omitting `agy_start.effort` is
|
|
1060
|
+
* always allowed; an empty list means it must be omitted (`claude-*`), and a
|
|
1061
|
+
* name with the effort baked in (`gemini-*-high`) lists exactly that one
|
|
1062
|
+
* value — any other is rejected by agy before the turn starts.
|
|
1063
|
+
*/
|
|
1064
|
+
export interface ModelCapability {
|
|
1065
|
+
name: string;
|
|
1066
|
+
efforts: readonly string[];
|
|
1067
|
+
}
|
|
1068
|
+
export interface Capabilities {
|
|
1069
|
+
server_version: string;
|
|
1070
|
+
schema_version: number;
|
|
1071
|
+
project_root: string;
|
|
1072
|
+
project_key: string;
|
|
1073
|
+
profiles: Array<{
|
|
1074
|
+
name: Profile;
|
|
1075
|
+
description: string;
|
|
1076
|
+
write: boolean;
|
|
1077
|
+
bypass_sandbox: boolean;
|
|
1078
|
+
}>;
|
|
1079
|
+
/** The human-owned ceiling this project's `agy_start` calls resolve against. */
|
|
1080
|
+
ceiling: CeilingSummary;
|
|
1081
|
+
models: ModelCapability[];
|
|
1082
|
+
efforts: string[];
|
|
1083
|
+
modes: string[];
|
|
1084
|
+
session_modes: SessionMode[];
|
|
1085
|
+
on_denial: OnDenial[];
|
|
1086
|
+
limits: {
|
|
1087
|
+
max_running_jobs: number;
|
|
1088
|
+
max_timeout_ms: number;
|
|
1089
|
+
default_timeout_ms: number;
|
|
1090
|
+
/**
|
|
1091
|
+
* `session_mode: 'session'` only. No live
|
|
1092
|
+
* measurement dictates this value — there is no recorded turn-cadence
|
|
1093
|
+
* data for how quickly a real caller re-sends after a turn finishes.
|
|
1094
|
+
* Chosen to comfortably outlast one round trip of "read the last turn,
|
|
1095
|
+
* decide the next prompt" without idling out a hard `timeout_ms`
|
|
1096
|
+
* (typically minutes) just to wait on a client that already went quiet.
|
|
1097
|
+
* Revisit if live use shows a different cadence.
|
|
1098
|
+
*/
|
|
1099
|
+
default_idle_timeout_ms: number;
|
|
1100
|
+
/** Ceiling `idle_timeout_ms` is clamped to, mirroring `max_timeout_ms`. */
|
|
1101
|
+
max_idle_timeout_ms: number;
|
|
1102
|
+
/** `verify_timeout_ms` default (10 minutes) when `verify_command` is set but no timeout is given. Clamped to `max_timeout_ms`, same as `timeout_ms`. */
|
|
1103
|
+
default_verify_timeout_ms: number;
|
|
1104
|
+
max_response_bytes: number;
|
|
1105
|
+
max_log_tail_lines: number;
|
|
1106
|
+
};
|
|
1107
|
+
agy_bin: string;
|
|
1108
|
+
/** False when the configured binary is missing; `agy_start` will fail. */
|
|
1109
|
+
agy_bin_present: boolean;
|
|
1110
|
+
/** The connected client's own `initialize` declaration. See {@link ClientSnapshot}. */
|
|
1111
|
+
client: ClientSnapshot | null;
|
|
1112
|
+
}
|
|
1113
|
+
//# sourceMappingURL=types.d.ts.map
|