@gajae-code/coding-agent 0.4.5 → 0.5.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 +62 -0
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +7 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +30 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +8 -1
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/harness-control-plane/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +3 -1
- package/dist/types/session/blob-store.d.ts +59 -4
- package/dist/types/session/session-manager.d.ts +24 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +7 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/tools/subagent.d.ts +6 -0
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli.ts +9 -4
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +43 -5
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- package/src/config/file-lock-gc.ts +181 -0
- package/src/config/file-lock.ts +14 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +264 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +513 -26
- package/src/cursor.ts +16 -2
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/export/html/index.ts +13 -9
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +58 -7
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +46 -29
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +211 -8
- package/src/gjc-runtime/tmux-common.ts +29 -0
- package/src/gjc-runtime/tmux-gc.ts +176 -0
- package/src/gjc-runtime/tmux-sessions.ts +68 -12
- package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +89 -27
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +93 -0
- package/src/harness-control-plane/types.ts +4 -0
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +14 -8
- package/src/main.ts +7 -2
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +370 -181
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +34 -42
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +187 -39
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/sdk.ts +46 -5
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +179 -25
- package/src/session/blob-store.ts +148 -6
- package/src/session/session-manager.ts +311 -60
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +78 -6
- package/src/task/receipt.ts +5 -0
- package/src/task/render.ts +21 -1
- package/src/task/types.ts +8 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/ask.ts +56 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +9 -0
- package/src/tools/subagent.ts +26 -2
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/tool-choice.ts +45 -16
|
@@ -17,12 +17,26 @@ import type { OpenGateInput } from "./workflow-gate-broker";
|
|
|
17
17
|
/** "Other (type your own)" sentinel, mirroring the interactive ask tool. */
|
|
18
18
|
export const GATE_OTHER_OPTION = "Other (type your own)";
|
|
19
19
|
|
|
20
|
+
/** Optional structured deep-interview round metadata supplied by the agent. */
|
|
21
|
+
export interface AskGateDeepInterviewState {
|
|
22
|
+
round_id?: string;
|
|
23
|
+
round: number;
|
|
24
|
+
component: string;
|
|
25
|
+
dimension: string;
|
|
26
|
+
ambiguity: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
export interface AskGateQuestion {
|
|
21
30
|
id: string;
|
|
22
31
|
question: string;
|
|
23
32
|
options: Array<{ label: string }>;
|
|
24
33
|
multi?: boolean;
|
|
25
34
|
recommended?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Structured round metadata. When present it is the authoritative source for gate
|
|
37
|
+
* `stage_state`; when absent, the question text is regex-parsed as a fallback.
|
|
38
|
+
*/
|
|
39
|
+
deepInterview?: AskGateDeepInterviewState;
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
export interface AskGateResult {
|
|
@@ -130,6 +144,19 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
|
|
|
130
144
|
};
|
|
131
145
|
}
|
|
132
146
|
|
|
147
|
+
/** Build `stage_state` round metadata from the structured param (authoritative when present). */
|
|
148
|
+
function structuredDeepInterviewState(meta: AskGateDeepInterviewState): Record<string, unknown> {
|
|
149
|
+
const state: Record<string, unknown> = {
|
|
150
|
+
deep_interview_metadata: true,
|
|
151
|
+
round: meta.round,
|
|
152
|
+
component: meta.component,
|
|
153
|
+
dimension: meta.dimension,
|
|
154
|
+
ambiguity: meta.ambiguity,
|
|
155
|
+
};
|
|
156
|
+
if (meta.round_id !== undefined) state.round_id = meta.round_id;
|
|
157
|
+
return state;
|
|
158
|
+
}
|
|
159
|
+
|
|
133
160
|
/** Build the `workflow_gate` open-input for one deep-interview question. */
|
|
134
161
|
export function questionToGate(question: AskGateQuestion): OpenGateInput {
|
|
135
162
|
const labels = question.options.map(o => o.label);
|
|
@@ -151,7 +178,9 @@ export function questionToGate(question: AskGateQuestion): OpenGateInput {
|
|
|
151
178
|
multi: question.multi ?? false,
|
|
152
179
|
options: labels,
|
|
153
180
|
other_option: GATE_OTHER_OPTION,
|
|
154
|
-
...
|
|
181
|
+
...(question.deepInterview
|
|
182
|
+
? structuredDeepInterviewState(question.deepInterview)
|
|
183
|
+
: deepInterviewQuestionState(question.question)),
|
|
155
184
|
},
|
|
156
185
|
},
|
|
157
186
|
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process registry of running gjc RPC sessions (issue 10).
|
|
3
|
+
*
|
|
4
|
+
* Each live RPC server writes a record under `<agent-dir>/rpc-sessions/<id>.json`
|
|
5
|
+
* on start and removes it on shutdown, so a separate process can discover which
|
|
6
|
+
* sessions are alive (and, once persistence lands in issue 09, how to reach
|
|
7
|
+
* them). Listing reaps records whose owning process is no longer alive, so a
|
|
8
|
+
* crashed server never leaves a permanent phantom entry.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { getAgentDir } from "@gajae-code/utils";
|
|
13
|
+
|
|
14
|
+
export type RpcSessionTransport = "stdio" | "bridge" | "socket";
|
|
15
|
+
|
|
16
|
+
export interface RpcSessionRecord {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
pid: number;
|
|
19
|
+
transport: RpcSessionTransport;
|
|
20
|
+
cwd: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
/** ISO-8601 start timestamp. */
|
|
23
|
+
startedAt: string;
|
|
24
|
+
/** Reachable endpoint for persistent transports (issue 09); absent for stdio. */
|
|
25
|
+
endpoint?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Registry directory: `<agent-dir>/rpc-sessions` (honors GJC_CODING_AGENT_DIR via getAgentDir). */
|
|
29
|
+
function rpcSessionsDir(agentDir?: string): string {
|
|
30
|
+
return path.join(agentDir ?? getAgentDir(), "rpc-sessions");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function recordPath(sessionId: string, agentDir?: string): string {
|
|
34
|
+
return path.join(rpcSessionsDir(agentDir), `${sessionId}.json`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Write (or replace) the registry record for a session. The record is written to
|
|
39
|
+
* a same-directory temp file and atomically renamed into place so a concurrent
|
|
40
|
+
* reader never observes (and reaps) a partially-written record.
|
|
41
|
+
*/
|
|
42
|
+
export async function registerRpcSession(record: RpcSessionRecord, agentDir?: string): Promise<string> {
|
|
43
|
+
const file = recordPath(record.sessionId, agentDir);
|
|
44
|
+
// `.tmp` suffix keeps the staging file out of the `*.json` listing/reaping path.
|
|
45
|
+
const staging = `${file}.${process.pid}.tmp`;
|
|
46
|
+
await Bun.write(staging, JSON.stringify(record));
|
|
47
|
+
await fs.rename(staging, file);
|
|
48
|
+
return file;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Remove a session's registry record. Best-effort: a missing file is not an error. */
|
|
52
|
+
export async function unregisterRpcSession(sessionId: string, agentDir?: string): Promise<void> {
|
|
53
|
+
await fs.rm(recordPath(sessionId, agentDir), { force: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isProcessAlive(pid: number): boolean {
|
|
57
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
58
|
+
try {
|
|
59
|
+
// Signal 0 performs error checking without delivering a signal.
|
|
60
|
+
process.kill(pid, 0);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// ESRCH => no such process (dead). EPERM => alive but owned by another user.
|
|
64
|
+
return (err as NodeJS.ErrnoException).code === "EPERM";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseRecord(raw: string): RpcSessionRecord | undefined {
|
|
69
|
+
let obj: Partial<RpcSessionRecord>;
|
|
70
|
+
try {
|
|
71
|
+
obj = JSON.parse(raw) as Partial<RpcSessionRecord>;
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (typeof obj.sessionId !== "string" || typeof obj.pid !== "number") return undefined;
|
|
76
|
+
return obj as RpcSessionRecord;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* List live RPC sessions, reaping records whose process is gone or whose file is
|
|
81
|
+
* unparseable. Returns records sorted by `startedAt` ascending.
|
|
82
|
+
*/
|
|
83
|
+
export async function listRpcSessions(agentDir?: string): Promise<RpcSessionRecord[]> {
|
|
84
|
+
const dir = rpcSessionsDir(agentDir);
|
|
85
|
+
let entries: string[];
|
|
86
|
+
try {
|
|
87
|
+
entries = await fs.readdir(dir);
|
|
88
|
+
} catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const live: RpcSessionRecord[] = [];
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (!entry.endsWith(".json")) continue;
|
|
94
|
+
const file = path.join(dir, entry);
|
|
95
|
+
let raw: string;
|
|
96
|
+
try {
|
|
97
|
+
raw = await fs.readFile(file, "utf8");
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const record = parseRecord(raw);
|
|
102
|
+
if (!record || !isProcessAlive(record.pid)) {
|
|
103
|
+
await fs.rm(file, { force: true });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
live.push(record);
|
|
107
|
+
}
|
|
108
|
+
return live.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
109
|
+
}
|
|
@@ -45,6 +45,30 @@ export function actionClassForScope(scope: BridgeCommandScope): RpcUnattendedAct
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Runtime list of every v1 action class — membership-validation source for negotiate (#319). */
|
|
49
|
+
export const RPC_UNATTENDED_ACTION_CLASSES: readonly RpcUnattendedActionClass[] = [
|
|
50
|
+
"command.prompt",
|
|
51
|
+
"command.control",
|
|
52
|
+
"command.bash",
|
|
53
|
+
"command.export",
|
|
54
|
+
"command.session",
|
|
55
|
+
"command.model",
|
|
56
|
+
"command.message_read",
|
|
57
|
+
"command.host_tools",
|
|
58
|
+
"command.host_uri",
|
|
59
|
+
"command.admin",
|
|
60
|
+
"bash.readonly",
|
|
61
|
+
"bash.mutating",
|
|
62
|
+
"bash.destructive",
|
|
63
|
+
"git.force_push",
|
|
64
|
+
"file.delete",
|
|
65
|
+
"file.write",
|
|
66
|
+
"host_tool.invoke",
|
|
67
|
+
"host_uri.read",
|
|
68
|
+
"host_uri.write",
|
|
69
|
+
"auth.login",
|
|
70
|
+
];
|
|
71
|
+
|
|
48
72
|
const READONLY_COMMANDS = new Set([
|
|
49
73
|
"ls",
|
|
50
74
|
"cat",
|
|
@@ -24,7 +24,8 @@ import type {
|
|
|
24
24
|
RpcUnattendedRefusalCode,
|
|
25
25
|
} from "../../rpc/rpc-types";
|
|
26
26
|
import type { BridgeCommandScope } from "./scopes";
|
|
27
|
-
import {
|
|
27
|
+
import { BRIDGE_COMMAND_SCOPES, MANDATORY_FLOOR_COMMAND_SCOPES } from "./scopes";
|
|
28
|
+
import { actionClassForScope, classifyBashAction, RPC_UNATTENDED_ACTION_CLASSES } from "./unattended-action-policy";
|
|
28
29
|
|
|
29
30
|
/** Coordinated abort surfaces invoked exactly once on a budget breach / abort. */
|
|
30
31
|
export interface UnattendedAbortHooks {
|
|
@@ -157,8 +158,11 @@ export class UnattendedRunController {
|
|
|
157
158
|
this.sessionId = ctx.sessionId;
|
|
158
159
|
this.actor = declaration.actor;
|
|
159
160
|
this.budget = budget;
|
|
160
|
-
this.scopes = new Set(declaration.scopes);
|
|
161
|
-
this.actionAllowlist = new Set(
|
|
161
|
+
this.scopes = new Set([...declaration.scopes, ...MANDATORY_FLOOR_COMMAND_SCOPES]);
|
|
162
|
+
this.actionAllowlist = new Set([
|
|
163
|
+
...declaration.action_allowlist,
|
|
164
|
+
...MANDATORY_FLOOR_COMMAND_SCOPES.map(actionClassForScope),
|
|
165
|
+
]);
|
|
162
166
|
this.now = ctx.now ?? Date.now;
|
|
163
167
|
this.audit = ctx.audit;
|
|
164
168
|
this.abortHooks = ctx.abortHooks ?? {};
|
|
@@ -183,6 +187,22 @@ export class UnattendedRunController {
|
|
|
183
187
|
"declaration.action_allowlist must be string[]",
|
|
184
188
|
);
|
|
185
189
|
}
|
|
190
|
+
const unknownScopes = d.scopes.filter(scope => !BRIDGE_COMMAND_SCOPES.includes(scope as BridgeCommandScope));
|
|
191
|
+
if (unknownScopes.length > 0) {
|
|
192
|
+
throw new UnattendedNegotiationError(
|
|
193
|
+
"invalid_unattended_declaration",
|
|
194
|
+
`declaration.scopes contains unknown scope(s): ${unknownScopes.join(", ")}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const unknownActions = d.action_allowlist.filter(
|
|
198
|
+
action => !RPC_UNATTENDED_ACTION_CLASSES.includes(action as RpcUnattendedActionClass),
|
|
199
|
+
);
|
|
200
|
+
if (unknownActions.length > 0) {
|
|
201
|
+
throw new UnattendedNegotiationError(
|
|
202
|
+
"invalid_unattended_declaration",
|
|
203
|
+
`declaration.action_allowlist contains unknown action class(es): ${unknownActions.join(", ")}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
186
206
|
const budget = validateBudget(d.budget);
|
|
187
207
|
// Reject providers that cannot account for tokens/cost (fail-closed): require
|
|
188
208
|
// an explicit positive capability signal — omitted/unknown is refused too.
|
|
@@ -31,6 +31,13 @@ import {
|
|
|
31
31
|
} from "./unattended-run-controller";
|
|
32
32
|
import { type GateStore, MemoryGateStore, type OpenGateInput, WorkflowGateBroker } from "./workflow-gate-broker";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* RPC commands that perform agent/tool work and therefore consume one unit of the
|
|
36
|
+
* `max_tool_calls` budget. Read-only/control/cancellation commands are wall-time-bounded
|
|
37
|
+
* and scope-checked but must NOT charge the tool-call budget (issue 04).
|
|
38
|
+
*/
|
|
39
|
+
const CHARGED_COMMAND_TYPES = new Set<RpcCommand["type"]>(["bash", "prompt", "steer", "follow_up", "abort_and_prompt"]);
|
|
40
|
+
|
|
34
41
|
/** Minimal surface a skill runtime / ask tool uses to emit a gate and await its answer. */
|
|
35
42
|
export interface WorkflowGateEmitter {
|
|
36
43
|
/** True only when unattended mode has been negotiated. */
|
|
@@ -116,7 +123,15 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
|
|
|
116
123
|
|
|
117
124
|
preflightCommand(command: RpcCommand): void {
|
|
118
125
|
if (!this.#controller) return;
|
|
119
|
-
|
|
126
|
+
const phase = `${command.type} preflight`;
|
|
127
|
+
// Always enforce wall-time; only charge the tool-call budget for commands that perform
|
|
128
|
+
// agent/tool work (issue 04). Read-only/control/cancellation commands must not consume
|
|
129
|
+
// max_tool_calls, but remain wall-time-bounded and scope/action-checked.
|
|
130
|
+
if (CHARGED_COMMAND_TYPES.has(command.type)) {
|
|
131
|
+
this.#controller.preflightToolCall(phase);
|
|
132
|
+
} else {
|
|
133
|
+
this.#controller.checkWallTime(phase);
|
|
134
|
+
}
|
|
120
135
|
if (command.type === "bash") {
|
|
121
136
|
this.#controller.authorizeBash(command.command);
|
|
122
137
|
return;
|
package/src/sdk.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
Snowflake,
|
|
33
33
|
} from "@gajae-code/utils";
|
|
34
34
|
|
|
35
|
-
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
|
|
35
|
+
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "./async";
|
|
36
36
|
import { loadCapability } from "./capability";
|
|
37
37
|
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
|
|
38
38
|
import { ModelRegistry } from "./config/model-registry";
|
|
@@ -50,6 +50,7 @@ import { CursorExecHandlers } from "./cursor";
|
|
|
50
50
|
import "./discovery";
|
|
51
51
|
import { resolveConfigValue } from "./config/resolve-config-value";
|
|
52
52
|
import { getEmbeddedDefaultGjcSkills } from "./defaults/gjc-defaults";
|
|
53
|
+
import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory } from "./defaults/gjc-grok-cli";
|
|
53
54
|
import { initializeWithSettings } from "./discovery";
|
|
54
55
|
import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
|
|
55
56
|
import { TtsrManager } from "./export/ttsr";
|
|
@@ -133,7 +134,7 @@ import { ToolContextStore } from "./tools/context";
|
|
|
133
134
|
import { getImageGenTools } from "./tools/image-gen";
|
|
134
135
|
import { wrapToolWithMetaNotice } from "./tools/output-meta";
|
|
135
136
|
import { EventBus } from "./utils/event-bus";
|
|
136
|
-
import { buildNamedToolChoice } from "./utils/tool-choice";
|
|
137
|
+
import { buildNamedToolChoice, buildNamedToolChoiceResult } from "./utils/tool-choice";
|
|
137
138
|
import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
138
139
|
|
|
139
140
|
type AsyncResultEntry = {
|
|
@@ -234,6 +235,8 @@ export interface CreateAgentSessionOptions {
|
|
|
234
235
|
modelPattern?: string;
|
|
235
236
|
/** Thinking selector. Default: from settings, else unset */
|
|
236
237
|
thinkingLevel?: ThinkingLevel;
|
|
238
|
+
/** Runtime substitution metadata for the initial model_change session event. */
|
|
239
|
+
modelSubstitution?: { requestedModel: Model; reason: string };
|
|
237
240
|
/** Models available for cycling (Ctrl+P in interactive mode) */
|
|
238
241
|
scopedModels?: ScopedModelSelection[];
|
|
239
242
|
|
|
@@ -1122,7 +1125,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1122
1125
|
const formattedResult = await formatAsyncResultForFollowUp(result);
|
|
1123
1126
|
if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
|
|
1124
1127
|
|
|
1125
|
-
const durationMs = job ?
|
|
1128
|
+
const durationMs = job ? jobElapsedMs(job) : undefined;
|
|
1126
1129
|
session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
|
|
1127
1130
|
jobId,
|
|
1128
1131
|
result: formattedResult,
|
|
@@ -1212,6 +1215,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1212
1215
|
const m = session.model;
|
|
1213
1216
|
return m ? buildNamedToolChoice(name, m) : undefined;
|
|
1214
1217
|
},
|
|
1218
|
+
buildToolChoiceResult: name => buildNamedToolChoiceResult(name, session.model),
|
|
1215
1219
|
steer: msg =>
|
|
1216
1220
|
session.agent.steer({
|
|
1217
1221
|
role: "custom",
|
|
@@ -1338,13 +1342,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1338
1342
|
}
|
|
1339
1343
|
|
|
1340
1344
|
// Extension/module discovery is quarantined; retain only the private
|
|
1341
|
-
// runtime needed for
|
|
1345
|
+
// runtime needed for bundled product extensions, explicitly supplied SDK
|
|
1346
|
+
// extension factories, and custom tools. Filesystem extension paths remain
|
|
1347
|
+
// ignored here even when options.additionalExtensionPaths is supplied.
|
|
1342
1348
|
const extensionsResult: LoadExtensionsResult = options.preloadedExtensions ?? {
|
|
1343
1349
|
extensions: [],
|
|
1344
1350
|
errors: [],
|
|
1345
1351
|
runtime: new ExtensionRuntime(),
|
|
1346
1352
|
};
|
|
1347
1353
|
|
|
1354
|
+
if (!extensionsResult.extensions.some(extension => extension.path === BUNDLED_GROK_BUILD_EXTENSION_ID)) {
|
|
1355
|
+
const bundledGrokExtension = await loadExtensionFromFactory(
|
|
1356
|
+
getBundledGrokBuildExtensionFactory(),
|
|
1357
|
+
cwd,
|
|
1358
|
+
eventBus,
|
|
1359
|
+
extensionsResult.runtime,
|
|
1360
|
+
BUNDLED_GROK_BUILD_EXTENSION_ID,
|
|
1361
|
+
);
|
|
1362
|
+
extensionsResult.extensions.push(bundledGrokExtension);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1348
1365
|
// Load inline extensions from factories
|
|
1349
1366
|
if (inlineExtensions.length > 0) {
|
|
1350
1367
|
for (let i = 0; i < inlineExtensions.length; i++) {
|
|
@@ -1898,6 +1915,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1898
1915
|
}
|
|
1899
1916
|
return result;
|
|
1900
1917
|
},
|
|
1918
|
+
onToolChoiceIncapability: event => {
|
|
1919
|
+
const droppedLabel = session?.toolChoiceQueue.degradeInFlight(event.reason);
|
|
1920
|
+
logger.debug("Dropped in-flight tool choice after runtime incapability", {
|
|
1921
|
+
droppedLabel,
|
|
1922
|
+
api: event.api,
|
|
1923
|
+
provider: event.provider,
|
|
1924
|
+
model: event.model,
|
|
1925
|
+
requestedLevel: event.requestedLevel,
|
|
1926
|
+
resolvedLevel: event.resolvedLevel,
|
|
1927
|
+
reason: event.reason,
|
|
1928
|
+
registryKey: event.registryKey,
|
|
1929
|
+
});
|
|
1930
|
+
},
|
|
1901
1931
|
intentTracing: !!intentField,
|
|
1902
1932
|
getToolChoice: () => session?.nextToolChoice(),
|
|
1903
1933
|
telemetry: options.telemetry,
|
|
@@ -1912,7 +1942,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1912
1942
|
} else {
|
|
1913
1943
|
// Save initial model, thinking level, and service tier for new sessions so they can be restored on resume.
|
|
1914
1944
|
if (model) {
|
|
1915
|
-
|
|
1945
|
+
const substitution = options.modelSubstitution;
|
|
1946
|
+
sessionManager.appendModelChange(
|
|
1947
|
+
`${model.provider}/${model.id}`,
|
|
1948
|
+
undefined,
|
|
1949
|
+
substitution
|
|
1950
|
+
? {
|
|
1951
|
+
previousModel: `${substitution.requestedModel.provider}/${substitution.requestedModel.id}`,
|
|
1952
|
+
reason: substitution.reason,
|
|
1953
|
+
thinkingLevel: thinkingLevel ?? null,
|
|
1954
|
+
}
|
|
1955
|
+
: undefined,
|
|
1956
|
+
);
|
|
1916
1957
|
}
|
|
1917
1958
|
sessionManager.appendThinkingLevelChange(thinkingLevel);
|
|
1918
1959
|
if (initialServiceTier) {
|
|
@@ -73,9 +73,24 @@ export class SecretObfuscator {
|
|
|
73
73
|
/** Replace-mode plain mappings: secret → replacement */
|
|
74
74
|
#replaceMappings = new Map<string, string>();
|
|
75
75
|
|
|
76
|
+
/** Replace-mode plain mappings sorted longest-first for deterministic longest-match replacement. */
|
|
77
|
+
#sortedReplaceMappings: Array<{ secret: string; replacement: string }> = [];
|
|
78
|
+
|
|
79
|
+
/** Obfuscate-mode plain and regex-discovered mappings sorted longest-first. */
|
|
80
|
+
#sortedObfuscateMappings: Array<{ secret: string; index: number; placeholder: string }> = [];
|
|
81
|
+
|
|
82
|
+
/** Reverse lookup for obfuscate-mode secrets to avoid scanning mappings. */
|
|
83
|
+
#obfuscateIndexBySecret = new Map<string, number>();
|
|
84
|
+
|
|
76
85
|
/** Reverse lookup for deobfuscation: placeholder → secret */
|
|
77
86
|
#deobfuscateMap = new Map<string, string>();
|
|
78
87
|
|
|
88
|
+
/** Combined plain-secret regex cache for single-pass replacement. */
|
|
89
|
+
#combinedPlainRegex: RegExp | undefined;
|
|
90
|
+
#combinedPlainReplacementBySecret = new Map<string, string>();
|
|
91
|
+
#combinedPlainRegexDirty = true;
|
|
92
|
+
#useSequentialPlainReplacement = false;
|
|
93
|
+
|
|
79
94
|
/** Next available index for regex match discoveries */
|
|
80
95
|
#nextIndex: number;
|
|
81
96
|
|
|
@@ -93,6 +108,7 @@ export class SecretObfuscator {
|
|
|
93
108
|
this.#plainMappings.set(entry.content, index);
|
|
94
109
|
this.#obfuscateMappings.set(index, { secret: entry.content, placeholder });
|
|
95
110
|
this.#deobfuscateMap.set(placeholder, entry.content);
|
|
111
|
+
this.#obfuscateIndexBySecret.set(entry.content, index);
|
|
96
112
|
index++;
|
|
97
113
|
} else {
|
|
98
114
|
// replace mode
|
|
@@ -111,6 +127,16 @@ export class SecretObfuscator {
|
|
|
111
127
|
}
|
|
112
128
|
|
|
113
129
|
this.#nextIndex = index;
|
|
130
|
+
this.#sortedReplaceMappings = [...this.#replaceMappings]
|
|
131
|
+
.sort((a, b) => b[0].length - a[0].length)
|
|
132
|
+
.map(([secret, replacement]) => ({ secret, replacement }));
|
|
133
|
+
this.#sortedObfuscateMappings = [...this.#plainMappings]
|
|
134
|
+
.sort((a, b) => b[0].length - a[0].length)
|
|
135
|
+
.map(([secret, mappingIndex]) => ({
|
|
136
|
+
secret,
|
|
137
|
+
index: mappingIndex,
|
|
138
|
+
placeholder: this.#obfuscateMappings.get(mappingIndex)!.placeholder,
|
|
139
|
+
}));
|
|
114
140
|
this.#hasAny = entries.length > 0;
|
|
115
141
|
}
|
|
116
142
|
|
|
@@ -121,18 +147,7 @@ export class SecretObfuscator {
|
|
|
121
147
|
/** Obfuscate all secrets in text. Bidirectional placeholders for obfuscate mode, one-way for replace. */
|
|
122
148
|
obfuscate(text: string): string {
|
|
123
149
|
if (!this.#hasAny) return text;
|
|
124
|
-
let result = text;
|
|
125
|
-
|
|
126
|
-
// 1. Process replace-mode plain secrets
|
|
127
|
-
for (const [secret, replacement] of [...this.#replaceMappings].sort((a, b) => b[0].length - a[0].length)) {
|
|
128
|
-
result = replaceAll(result, secret, replacement);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 2. Process obfuscate-mode plain secrets
|
|
132
|
-
for (const [secret, index] of [...this.#plainMappings].sort((a, b) => b[0].length - a[0].length)) {
|
|
133
|
-
const mapping = this.#obfuscateMappings.get(index)!;
|
|
134
|
-
result = replaceAll(result, secret, mapping.placeholder);
|
|
135
|
-
}
|
|
150
|
+
let result = this.#obfuscatePlainMappings(text);
|
|
136
151
|
|
|
137
152
|
// 3. Process regex entries — discover new matches
|
|
138
153
|
for (const entry of this.#regexEntries) {
|
|
@@ -160,6 +175,9 @@ export class SecretObfuscator {
|
|
|
160
175
|
const placeholder = buildPlaceholder(index);
|
|
161
176
|
this.#obfuscateMappings.set(index, { secret: matchValue, placeholder });
|
|
162
177
|
this.#deobfuscateMap.set(placeholder, matchValue);
|
|
178
|
+
this.#obfuscateIndexBySecret.set(matchValue, index);
|
|
179
|
+
this.#insertSortedObfuscateMapping({ secret: matchValue, index, placeholder });
|
|
180
|
+
this.#combinedPlainRegexDirty = true;
|
|
163
181
|
}
|
|
164
182
|
const mapping = this.#obfuscateMappings.get(index)!;
|
|
165
183
|
result = replaceAll(result, matchValue, mapping.placeholder);
|
|
@@ -186,15 +204,74 @@ export class SecretObfuscator {
|
|
|
186
204
|
|
|
187
205
|
/** Find the obfuscate index for a known secret value. */
|
|
188
206
|
#findObfuscateIndex(secret: string): number | undefined {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
return this.#obfuscateIndexBySecret.get(secret);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#insertSortedObfuscateMapping(mapping: { secret: string; index: number; placeholder: string }): void {
|
|
211
|
+
let lo = 0;
|
|
212
|
+
let hi = this.#sortedObfuscateMappings.length;
|
|
213
|
+
while (lo < hi) {
|
|
214
|
+
const mid = (lo + hi) >> 1;
|
|
215
|
+
if (this.#sortedObfuscateMappings[mid]!.secret.length < mapping.secret.length) {
|
|
216
|
+
hi = mid;
|
|
217
|
+
} else {
|
|
218
|
+
lo = mid + 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.#sortedObfuscateMappings.splice(lo, 0, mapping);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#obfuscatePlainMappings(text: string): string {
|
|
225
|
+
this.#ensureCombinedPlainRegex();
|
|
226
|
+
if (this.#useSequentialPlainReplacement) return this.#obfuscatePlainMappingsSequential(text);
|
|
227
|
+
if (!this.#combinedPlainRegex) return text;
|
|
228
|
+
return text.replace(
|
|
229
|
+
this.#combinedPlainRegex,
|
|
230
|
+
match => this.#combinedPlainReplacementBySecret.get(match) ?? match,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
192
233
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
234
|
+
#obfuscatePlainMappingsSequential(text: string): string {
|
|
235
|
+
let result = text;
|
|
236
|
+
for (const mapping of this.#sortedReplaceMappings) {
|
|
237
|
+
result = replaceAll(result, mapping.secret, mapping.replacement);
|
|
238
|
+
}
|
|
239
|
+
for (const mapping of this.#sortedObfuscateMappings) {
|
|
240
|
+
result = replaceAll(result, mapping.secret, mapping.placeholder);
|
|
196
241
|
}
|
|
197
|
-
return
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#ensureCombinedPlainRegex(): void {
|
|
246
|
+
if (!this.#combinedPlainRegexDirty) return;
|
|
247
|
+
this.#combinedPlainRegexDirty = false;
|
|
248
|
+
this.#combinedPlainReplacementBySecret = new Map<string, string>();
|
|
249
|
+
|
|
250
|
+
const mappings = [
|
|
251
|
+
...this.#sortedReplaceMappings.map(mapping => ({ secret: mapping.secret, replacement: mapping.replacement })),
|
|
252
|
+
...this.#sortedObfuscateMappings.map(mapping => ({
|
|
253
|
+
secret: mapping.secret,
|
|
254
|
+
replacement: mapping.placeholder,
|
|
255
|
+
})),
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
this.#useSequentialPlainReplacement = mappings.some((mapping, index) =>
|
|
259
|
+
mappings.some(
|
|
260
|
+
(other, otherIndex) =>
|
|
261
|
+
other.secret.length > 0 &&
|
|
262
|
+
(mapping.replacement.includes(other.secret) ||
|
|
263
|
+
(index !== otherIndex &&
|
|
264
|
+
(mapping.secret.includes(other.secret) || other.secret.includes(mapping.secret)))),
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
for (const mapping of mappings) {
|
|
268
|
+
if (!this.#combinedPlainReplacementBySecret.has(mapping.secret))
|
|
269
|
+
this.#combinedPlainReplacementBySecret.set(mapping.secret, mapping.replacement);
|
|
270
|
+
}
|
|
271
|
+
this.#combinedPlainRegex =
|
|
272
|
+
mappings.length > 0
|
|
273
|
+
? new RegExp(mappings.map(mapping => escapeRegex(mapping.secret)).join("|"), "g")
|
|
274
|
+
: undefined;
|
|
198
275
|
}
|
|
199
276
|
}
|
|
200
277
|
|
|
@@ -238,14 +315,12 @@ export function obfuscateMessages(obfuscator: SecretObfuscator, messages: Messag
|
|
|
238
315
|
|
|
239
316
|
/** Replace all occurrences of `search` in `text` with `replacement`. */
|
|
240
317
|
function replaceAll(text: string, search: string, replacement: string): string {
|
|
241
|
-
if (search.length === 0) return text;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
return result;
|
|
318
|
+
if (search.length === 0 || !text.includes(search)) return text;
|
|
319
|
+
return text.split(search).join(replacement);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function escapeRegex(value: string): string {
|
|
323
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
249
324
|
}
|
|
250
325
|
|
|
251
326
|
/** Deep-walk an object, transforming all string values. */
|