@gajae-code/coding-agent 0.15.6 → 0.16.0
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 +59 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +77 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/session-runtime.d.ts +52 -0
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +77 -11
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +1 -5
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +23 -0
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +8 -1
- package/src/config/settings.ts +5 -0
- package/src/coordinator-mcp/projection-scan.ts +21 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +94 -20
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +79 -6
- package/src/gjc-runtime/session-state-sidecar.ts +929 -23
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +57 -2
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +605 -156
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/input-controller.ts +28 -0
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/prompts/system/master-mode.md +28 -0
- package/src/sdk/broker/broker.ts +1636 -26
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +186 -41
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +264 -5
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +99 -38
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/session-runtime.test.ts +210 -6
- package/src/sdk/host/session-runtime.ts +291 -49
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +13 -3
- package/src/sdk/router/session-router.ts +67 -59
- package/src/sdk/session.ts +357 -155
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/agent-session.ts +640 -103
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +337 -24
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/bash.ts +256 -42
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +4 -0
|
@@ -6,6 +6,8 @@ export {
|
|
|
6
6
|
type ControlResponse,
|
|
7
7
|
controlRequestFromFrame,
|
|
8
8
|
dispatchControl,
|
|
9
|
+
type TerminalAbortIdentity,
|
|
9
10
|
TypedControlError,
|
|
11
|
+
terminalAbortIdentity,
|
|
10
12
|
} from "./dispatch";
|
|
11
13
|
export type { AbortScope, ControlInput, ControlSurface, ControlValue } from "./operations";
|
|
@@ -11,6 +11,8 @@ export type AbortScope = "turn" | "owned";
|
|
|
11
11
|
export interface TerminalAbortInput {
|
|
12
12
|
mode: "terminal";
|
|
13
13
|
scope?: AbortScope;
|
|
14
|
+
/** Broker-authorized local override; public SDK callers cannot grant this authority. */
|
|
15
|
+
operator?: boolean;
|
|
14
16
|
}
|
|
15
17
|
export type ControlInput = Record<string, unknown>;
|
|
16
18
|
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
import { findOperation } from "../../protocol/operation-registry";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Private
|
|
4
|
+
* Private fields carried only on Broker-authorized endpoint controls.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
* never published in endpoint discovery or lifecycle service results;
|
|
6
|
-
* that do not hold the Broker's indexed lifecycle record cannot satisfy
|
|
7
|
-
* gate.
|
|
6
|
+
* Their values are lifecycle effect markers bound to the serving process. They
|
|
7
|
+
* are never published in endpoint discovery or lifecycle service results;
|
|
8
|
+
* callers that do not hold the Broker's indexed lifecycle record cannot satisfy
|
|
9
|
+
* either gate.
|
|
8
10
|
*/
|
|
9
11
|
export const BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD = "__gjcBrokerCloseCapability";
|
|
12
|
+
/** Private field carried only on the Broker-authorized terminal-abort request. */
|
|
13
|
+
export const BROKER_RUNTIME_ABORT_CAPABILITY_FIELD = "__gjcBrokerAbortCapability";
|
|
10
14
|
const EXPECTED_BROKER_RUNTIME_CLOSE_CAPABILITY = process.env.GJC_LIFECYCLE_REQUEST_ID;
|
|
15
|
+
const EXPECTED_BROKER_RUNTIME_ABORT_CAPABILITY = process.env.GJC_LIFECYCLE_REQUEST_ID;
|
|
16
|
+
let brokerRuntimeAbortCapabilityOverrideForTest: string | undefined;
|
|
11
17
|
|
|
12
18
|
function record(value: unknown): Record<string, unknown> | undefined {
|
|
13
19
|
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
@@ -18,13 +24,188 @@ export function brokerRuntimeCloseCapability(input: unknown): string | undefined
|
|
|
18
24
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
export function brokerRuntimeAbortCapability(input: unknown): string | undefined {
|
|
28
|
+
const value = record(input)?.[BROKER_RUNTIME_ABORT_CAPABILITY_FIELD];
|
|
29
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Remove private Broker capabilities before a request is exposed to diagnostics. */
|
|
33
|
+
export function redactBrokerRuntimeCapabilities(frame: Record<string, unknown>): Record<string, unknown> {
|
|
34
|
+
if (frame.type !== "control_request") return frame;
|
|
24
35
|
const input = record(frame.input);
|
|
25
|
-
if (!input
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
if (!input) return frame;
|
|
37
|
+
let redacted = false;
|
|
38
|
+
const publicInput: Record<string, unknown> = {};
|
|
39
|
+
for (const [key, value] of Object.entries(input)) {
|
|
40
|
+
if (key === BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD || key === BROKER_RUNTIME_ABORT_CAPABILITY_FIELD) {
|
|
41
|
+
redacted = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
publicInput[key] = value;
|
|
45
|
+
}
|
|
46
|
+
return redacted ? { ...frame, input: publicInput } : frame;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Frame-level keys an observer may see. `SdkFrame` is an open
|
|
51
|
+
* `Record<string, unknown>` and `#observeRequest` runs BEFORE dispatch
|
|
52
|
+
* validation, so an adversarial or malformed frame can carry content under any
|
|
53
|
+
* top-level key. Only structural routing values survive. `idempotencyKey`,
|
|
54
|
+
* `id`, and other correlation values are caller-chosen and are deliberately
|
|
55
|
+
* absent; so are `cursor` (a signed pagination token that can carry
|
|
56
|
+
* session/revision data) and `expectedRevision` (no diagnostic consumer reads
|
|
57
|
+
* it). Even allowlisted routing names are checked against their protocol
|
|
58
|
+
* values below: a field-name allowlist alone is fail-open for scalar secrets.
|
|
59
|
+
*/
|
|
60
|
+
const OBSERVABLE_FRAME_FIELDS = new Set(["type", "operation", "query"]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Frame keys whose value must match an exact expected type to survive.
|
|
64
|
+
* `confirm` is a structural destructive-op gate, so only the boolean form is
|
|
65
|
+
* routing information; a caller-authored string under that name is content.
|
|
66
|
+
*/
|
|
67
|
+
const OBSERVABLE_TYPED_FRAME_FIELDS: Readonly<Record<string, "boolean">> = { confirm: "boolean" };
|
|
68
|
+
|
|
69
|
+
const OBSERVABLE_FRAME_LITERAL_FIELDS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
70
|
+
type: new Set(["control_request", "query_request"]),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const OBSERVER_MARKER_MAX_COUNT = 4096;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Input fields an observer may see. This is an ALLOWLIST on purpose: a
|
|
77
|
+
* denylist is fail-open, so every unlisted or newly added field would leak by
|
|
78
|
+
* default. Only structural routing/correlation values are preserved; all other
|
|
79
|
+
* values are caller content and are replaced with a shape marker.
|
|
80
|
+
*/
|
|
81
|
+
const OBSERVABLE_INPUT_FIELDS = new Set(["mode", "scope", "kind", "level", "on", "confirm"]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Input fields that carry a finite protocol value. Correlation identifiers,
|
|
85
|
+
* names, and operation arguments remain redacted even when they look like
|
|
86
|
+
* ordinary identifiers: callers are free to put arbitrary content in them.
|
|
87
|
+
*/
|
|
88
|
+
const OBSERVABLE_INPUT_LITERAL_FIELDS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
89
|
+
mode: new Set([
|
|
90
|
+
"turn",
|
|
91
|
+
"terminal",
|
|
92
|
+
"owned",
|
|
93
|
+
"allow",
|
|
94
|
+
"always-allow",
|
|
95
|
+
"deny",
|
|
96
|
+
"always-deny",
|
|
97
|
+
"prompt",
|
|
98
|
+
"all",
|
|
99
|
+
"one-at-a-time",
|
|
100
|
+
"immediate",
|
|
101
|
+
"wait",
|
|
102
|
+
]),
|
|
103
|
+
scope: new Set(["turn", "owned"]),
|
|
104
|
+
kind: new Set(["prompt", "skill"]),
|
|
105
|
+
level: new Set(["inherit", "off", "minimal", "low", "medium", "high", "xhigh", "max"]),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const OBSERVABLE_INPUT_TYPED_FIELDS: Readonly<Record<string, "boolean">> = {
|
|
109
|
+
on: "boolean",
|
|
110
|
+
confirm: "boolean",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Replaces caller content with a shape-preserving marker for observers. */
|
|
114
|
+
function redactedContentMarker(value: unknown): string {
|
|
115
|
+
if (typeof value === "string") {
|
|
116
|
+
const count = Math.min(value.length, OBSERVER_MARKER_MAX_COUNT);
|
|
117
|
+
return `[redacted ${count}${value.length > count ? "+" : ""} chars]`;
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
const count = Math.min(value.length, OBSERVER_MARKER_MAX_COUNT);
|
|
121
|
+
return `[redacted ${count}${value.length > count ? "+" : ""} items]`;
|
|
122
|
+
}
|
|
123
|
+
if (value !== null && typeof value === "object") {
|
|
124
|
+
const fields = Object.keys(value).length;
|
|
125
|
+
const count = Math.min(fields, OBSERVER_MARKER_MAX_COUNT);
|
|
126
|
+
return `[redacted ${count}${fields > count ? "+" : ""} fields]`;
|
|
127
|
+
}
|
|
128
|
+
return "[redacted]";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function setObservedField(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
132
|
+
Object.defineProperty(target, key, { configurable: true, enumerable: true, writable: true, value });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function observableFrameValue(key: string, value: unknown): unknown {
|
|
136
|
+
const requiredType = OBSERVABLE_TYPED_FRAME_FIELDS[key];
|
|
137
|
+
if (requiredType !== undefined) return typeof value === requiredType ? value : redactedContentMarker(value);
|
|
138
|
+
if (key === "operation" || key === "query") {
|
|
139
|
+
const known =
|
|
140
|
+
findOperation("control", String(value)) ??
|
|
141
|
+
findOperation("global", String(value)) ??
|
|
142
|
+
findOperation("query", String(value)) ??
|
|
143
|
+
findOperation("reverse", String(value));
|
|
144
|
+
return typeof value === "string" && value.length <= 128 && known !== undefined
|
|
145
|
+
? value
|
|
146
|
+
: redactedContentMarker(value);
|
|
147
|
+
}
|
|
148
|
+
const allowed = OBSERVABLE_FRAME_LITERAL_FIELDS[key];
|
|
149
|
+
return allowed?.has(typeof value === "string" ? value : "") ? value : redactedContentMarker(value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Strips caller content from a request frame before it reaches a diagnostic
|
|
154
|
+
* observer. Known protocol operation names and finite enum values survive so
|
|
155
|
+
* instrumentation stays useful; caller-chosen ids and all other values are
|
|
156
|
+
* redacted, including nested objects, so a field this module has never heard
|
|
157
|
+
* of cannot leak.
|
|
158
|
+
*/
|
|
159
|
+
export function redactObservedRequestContent(frame: Record<string, unknown>): Record<string, unknown> {
|
|
160
|
+
const observed: Record<string, unknown> = {};
|
|
161
|
+
for (const [key, value] of Object.entries(frame)) {
|
|
162
|
+
if (value === undefined) continue;
|
|
163
|
+
if (key === "input") {
|
|
164
|
+
const input = record(value);
|
|
165
|
+
// A non-record payload is content, not "nothing to redact". Returning
|
|
166
|
+
// early here would hand a string or array `input` through verbatim.
|
|
167
|
+
setObservedField(
|
|
168
|
+
observed,
|
|
169
|
+
key,
|
|
170
|
+
input === undefined ? redactedContentMarker(value) : redactObservedInput(input),
|
|
171
|
+
);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Structural routing scalars survive; every other top-level key, including
|
|
175
|
+
// one this module has never heard of, is redacted.
|
|
176
|
+
setObservedField(
|
|
177
|
+
observed,
|
|
178
|
+
key,
|
|
179
|
+
OBSERVABLE_FRAME_FIELDS.has(key) || Object.hasOwn(OBSERVABLE_TYPED_FRAME_FIELDS, key)
|
|
180
|
+
? observableFrameValue(key, value)
|
|
181
|
+
: redactedContentMarker(value),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return observed;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Redacts one input payload, preserving only structural scalar routing fields. */
|
|
188
|
+
function redactObservedInput(input: Record<string, unknown>): Record<string, unknown> {
|
|
189
|
+
const redacted: Record<string, unknown> = {};
|
|
190
|
+
for (const [field, value] of Object.entries(input)) {
|
|
191
|
+
if (value === undefined) continue;
|
|
192
|
+
const requiredType = OBSERVABLE_INPUT_TYPED_FIELDS[field];
|
|
193
|
+
if (requiredType !== undefined) {
|
|
194
|
+
setObservedField(redacted, field, typeof value === requiredType ? value : redactedContentMarker(value));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const allowed = OBSERVABLE_INPUT_LITERAL_FIELDS[field];
|
|
198
|
+
// A finite protocol value is safe; every caller-chosen scalar, including
|
|
199
|
+
// an allowlisted field with a forged string, is content and is redacted.
|
|
200
|
+
setObservedField(
|
|
201
|
+
redacted,
|
|
202
|
+
field,
|
|
203
|
+
OBSERVABLE_INPUT_FIELDS.has(field) && allowed?.has(typeof value === "string" ? value : "")
|
|
204
|
+
? value
|
|
205
|
+
: redactedContentMarker(value),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return redacted;
|
|
28
209
|
}
|
|
29
210
|
|
|
30
211
|
/**
|
|
@@ -38,3 +219,14 @@ export function hasBrokerRuntimeCloseCapability(input: unknown): boolean {
|
|
|
38
219
|
const actual = brokerRuntimeCloseCapability(input);
|
|
39
220
|
return typeof expected === "string" && expected.length > 0 && actual === expected;
|
|
40
221
|
}
|
|
222
|
+
|
|
223
|
+
/** Test-only override for the abort gate; production authority remains process-local. */
|
|
224
|
+
export function setBrokerRuntimeAbortCapabilityForTest(capability: string | undefined): void {
|
|
225
|
+
brokerRuntimeAbortCapabilityOverrideForTest = capability;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function hasBrokerRuntimeAbortCapability(input: unknown): boolean {
|
|
229
|
+
const expected = brokerRuntimeAbortCapabilityOverrideForTest ?? EXPECTED_BROKER_RUNTIME_ABORT_CAPABILITY;
|
|
230
|
+
const actual = brokerRuntimeAbortCapability(input);
|
|
231
|
+
return typeof expected === "string" && expected.length > 0 && actual === expected;
|
|
232
|
+
}
|
package/src/sdk/host/host.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logger } from "@gajae-code/utils";
|
|
2
2
|
import { AUTOROUTING_INACTIVE_WARNING } from "../../config/autorouting-contract";
|
|
3
|
-
import {
|
|
3
|
+
import { redactBrokerRuntimeCapabilities, redactObservedRequestContent } from "./control/runtime-gate";
|
|
4
4
|
import { type EventFrame, SessionEventStream } from "./events";
|
|
5
5
|
import { isAutoroutingInactive } from "./internal-autorouting-state";
|
|
6
6
|
import { type ProviderLease, ReverseLeaseError, ReverseLeaseRuntime } from "./reverse-leases";
|
|
@@ -88,6 +88,12 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
|
|
|
88
88
|
* that fails is never read as authorization.
|
|
89
89
|
*/
|
|
90
90
|
activationGate?: SessionActivationGate;
|
|
91
|
+
/** Private one-shot master-capability verification; never durable or public SDK surface. */
|
|
92
|
+
masterCapabilityVerify?: (frame: { nonce: unknown; attestationEpoch: unknown; capability: unknown }) => {
|
|
93
|
+
ok: boolean;
|
|
94
|
+
nonce: string;
|
|
95
|
+
attestationEpoch: string;
|
|
96
|
+
};
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
/** Shared by the replay filter and transport live broadcasts: a connection
|
|
@@ -416,6 +422,21 @@ export class SessionSdkHost {
|
|
|
416
422
|
return;
|
|
417
423
|
}
|
|
418
424
|
switch (frame.type) {
|
|
425
|
+
case "master_capability_verify": {
|
|
426
|
+
const result = this.#options.masterCapabilityVerify?.({
|
|
427
|
+
nonce: frame.nonce,
|
|
428
|
+
attestationEpoch: frame.attestationEpoch,
|
|
429
|
+
capability: frame.capability,
|
|
430
|
+
}) ?? { ok: false, nonce: "", attestationEpoch: "" };
|
|
431
|
+
await this.#send(connectionId, {
|
|
432
|
+
type: "master_capability_verify_result",
|
|
433
|
+
id: typeof frame.id === "string" ? frame.id : "",
|
|
434
|
+
ok: result.ok,
|
|
435
|
+
nonce: result.nonce,
|
|
436
|
+
attestationEpoch: result.attestationEpoch,
|
|
437
|
+
});
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
419
440
|
case "control_request": {
|
|
420
441
|
this.#observeRequest("control", connectionId, frame);
|
|
421
442
|
// Deferred readiness withholds `session_ready`, but the control
|
|
@@ -616,7 +637,11 @@ export class SessionSdkHost {
|
|
|
616
637
|
}
|
|
617
638
|
#observeRequest(kind: "control" | "query", connectionId: string, frame: SdkFrame): void {
|
|
618
639
|
try {
|
|
619
|
-
this.#options.onRequest?.(
|
|
640
|
+
this.#options.onRequest?.(
|
|
641
|
+
kind,
|
|
642
|
+
connectionId,
|
|
643
|
+
redactObservedRequestContent(redactBrokerRuntimeCapabilities(frame)),
|
|
644
|
+
);
|
|
620
645
|
} catch {
|
|
621
646
|
// Diagnostic observers must not change request handling.
|
|
622
647
|
}
|
|
@@ -5,7 +5,7 @@ import * as os from "node:os";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { AsyncJobManager } from "../../async";
|
|
7
7
|
import type { Settings } from "../../config/settings";
|
|
8
|
-
import type { ExtensionAPI, ExtensionContext } from "../../extensibility/extensions";
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionTranscriptEntry } from "../../extensibility/extensions";
|
|
9
9
|
import {
|
|
10
10
|
registerOwnedRegistration,
|
|
11
11
|
resetTerminalAbortRegistriesForTests,
|
|
@@ -16,7 +16,8 @@ import { Broker } from "../broker/broker";
|
|
|
16
16
|
import { createKindAwareReconciliation } from "../bus/kind-aware-reconciliation";
|
|
17
17
|
import { createPromptReconciliation } from "../bus/prompt-reconciliation";
|
|
18
18
|
import { createReconciliationStore } from "../bus/reconciliation-store";
|
|
19
|
-
import {
|
|
19
|
+
import { BROKER_RUNTIME_ABORT_CAPABILITY_FIELD, setBrokerRuntimeAbortCapabilityForTest } from "./control/runtime-gate";
|
|
20
|
+
import { CursorRegistry, QueryHandlers, type QueryResponse, RevisionStore } from "./query";
|
|
20
21
|
import {
|
|
21
22
|
createInvocationReconciliation,
|
|
22
23
|
createSdkSessionRuntimeExtension,
|
|
@@ -90,6 +91,7 @@ function extensionContext(
|
|
|
90
91
|
goalState?: unknown;
|
|
91
92
|
branch?: unknown[];
|
|
92
93
|
liveState?: { isStreaming: boolean; steeringQueueDepth: number; followupQueueDepth: number };
|
|
94
|
+
transcript?: ExtensionTranscriptEntry[];
|
|
93
95
|
} = {},
|
|
94
96
|
): ExtensionContext {
|
|
95
97
|
return {
|
|
@@ -102,6 +104,7 @@ function extensionContext(
|
|
|
102
104
|
getSessionName: () => undefined,
|
|
103
105
|
getBranch: () => options.branch ?? [],
|
|
104
106
|
},
|
|
107
|
+
getTranscript: () => options.transcript ?? [],
|
|
105
108
|
getGoalState: () => options.goalState,
|
|
106
109
|
} as unknown as ExtensionContext;
|
|
107
110
|
}
|
|
@@ -148,6 +151,97 @@ async function queryGoalState(
|
|
|
148
151
|
return response.page?.items[0];
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
async function queryLastAssistant(ctx: ExtensionContext, sessionId: string): Promise<QueryResponse> {
|
|
155
|
+
const surface = createSdkSurfaceFactory({
|
|
156
|
+
ctx,
|
|
157
|
+
id: sessionId,
|
|
158
|
+
api: {} as ExtensionAPI,
|
|
159
|
+
}).query;
|
|
160
|
+
const store = new RevisionStore(sessionId);
|
|
161
|
+
const response = await new QueryHandlers(
|
|
162
|
+
surface,
|
|
163
|
+
sessionId,
|
|
164
|
+
store,
|
|
165
|
+
new CursorRegistry("last-assistant-test-token", store),
|
|
166
|
+
).dispatch({
|
|
167
|
+
query: "session.last_assistant",
|
|
168
|
+
connectionId: "last-assistant-test-connection",
|
|
169
|
+
});
|
|
170
|
+
return response;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
test("session.last_assistant returns the latest projected readable text past non-text assistant tails", async () => {
|
|
174
|
+
const sessionId = "last-assistant-readable-tail";
|
|
175
|
+
const transcript: ExtensionTranscriptEntry[] = [
|
|
176
|
+
{
|
|
177
|
+
id: "visible-readable",
|
|
178
|
+
role: "assistant",
|
|
179
|
+
textSummary: "first line second line",
|
|
180
|
+
ts: new Date(1).toISOString(),
|
|
181
|
+
content: [
|
|
182
|
+
{ type: "text", text: "first line" },
|
|
183
|
+
{ type: "thinking", thinking: "private reasoning" },
|
|
184
|
+
{ type: "text", text: "second line" },
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
...Array.from(
|
|
188
|
+
{ length: 2_000 },
|
|
189
|
+
(_, index): ExtensionTranscriptEntry => ({
|
|
190
|
+
id: `tool-tail-${index}`,
|
|
191
|
+
role: "assistant",
|
|
192
|
+
textSummary: "",
|
|
193
|
+
ts: new Date(index + 2).toISOString(),
|
|
194
|
+
body: index % 3 === 0 ? "" : index % 3 === 1 ? " \n\t " : undefined,
|
|
195
|
+
content:
|
|
196
|
+
index % 3 === 2
|
|
197
|
+
? [
|
|
198
|
+
{ type: "thinking", thinking: "not readable" },
|
|
199
|
+
{ type: "toolCall", id: `call-${index}`, name: "read", arguments: {} },
|
|
200
|
+
]
|
|
201
|
+
: undefined,
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
204
|
+
];
|
|
205
|
+
const ctx = extensionContext(sessionId, "/tmp", {
|
|
206
|
+
transcript,
|
|
207
|
+
branch: [
|
|
208
|
+
{
|
|
209
|
+
type: "message",
|
|
210
|
+
message: { role: "assistant", content: [{ type: "text", text: "raw private tail" }] },
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({
|
|
216
|
+
ok: true,
|
|
217
|
+
page: { items: ["first line\nsecond line"] },
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("session.last_assistant returns resource_gone when the projected transcript has no readable assistant text", async () => {
|
|
222
|
+
const sessionId = "last-assistant-empty";
|
|
223
|
+
const ctx = extensionContext(sessionId, "/tmp", {
|
|
224
|
+
transcript: [
|
|
225
|
+
{
|
|
226
|
+
id: "user",
|
|
227
|
+
role: "user",
|
|
228
|
+
textSummary: "hello",
|
|
229
|
+
ts: new Date(1).toISOString(),
|
|
230
|
+
body: "hello",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "thinking-only",
|
|
234
|
+
role: "assistant",
|
|
235
|
+
textSummary: "",
|
|
236
|
+
ts: new Date(2).toISOString(),
|
|
237
|
+
content: [{ type: "thinking", thinking: "private reasoning" }],
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({ ok: false, error: { code: "resource_gone" } });
|
|
243
|
+
});
|
|
244
|
+
|
|
151
245
|
test("native prompt reconciliation fails closed for an explicitly empty assistant result", () => {
|
|
152
246
|
const reconciliation = createPromptReconciliation();
|
|
153
247
|
const correlation = { commandId: "native-empty-command", turnId: "native-empty-turn" };
|
|
@@ -1010,6 +1104,8 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1010
1104
|
});
|
|
1011
1105
|
test("SDK-only host admits, replays, and conflicts terminal abort requests durably", async () => {
|
|
1012
1106
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-abort-"));
|
|
1107
|
+
const operatorCapability = "runtime-terminal-abort-capability";
|
|
1108
|
+
setBrokerRuntimeAbortCapabilityForTest(operatorCapability);
|
|
1013
1109
|
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1014
1110
|
const api = {
|
|
1015
1111
|
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
@@ -1045,7 +1141,13 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1045
1141
|
},
|
|
1046
1142
|
abortPromptAndWaitWithTerminal: async (handle, options) => {
|
|
1047
1143
|
seamCalls.push({ handle, scope: options.terminal?.scope ?? "none" });
|
|
1048
|
-
return {
|
|
1144
|
+
return {
|
|
1145
|
+
status: "settled",
|
|
1146
|
+
terminalScope: {
|
|
1147
|
+
abortedAttemptEpoch: activeEpoch,
|
|
1148
|
+
lineageIdHash: "runtime-test-lineage",
|
|
1149
|
+
},
|
|
1150
|
+
};
|
|
1049
1151
|
},
|
|
1050
1152
|
},
|
|
1051
1153
|
});
|
|
@@ -1140,7 +1242,43 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1140
1242
|
}),
|
|
1141
1243
|
]),
|
|
1142
1244
|
);
|
|
1245
|
+
|
|
1246
|
+
// A separately connected, explicitly confirmed local operator may stop
|
|
1247
|
+
// the active turn and its exact owned work without weakening ordinary
|
|
1248
|
+
// connection ownership checks.
|
|
1249
|
+
transport.feed("local-operator", {
|
|
1250
|
+
type: "control_request",
|
|
1251
|
+
id: "terminal-operator-abort",
|
|
1252
|
+
operation: "turn.abort",
|
|
1253
|
+
input: {
|
|
1254
|
+
mode: "terminal",
|
|
1255
|
+
scope: "owned",
|
|
1256
|
+
operator: true,
|
|
1257
|
+
[BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: operatorCapability,
|
|
1258
|
+
},
|
|
1259
|
+
confirm: true,
|
|
1260
|
+
idempotencyKey: "terminal-operator-key",
|
|
1261
|
+
} as SdkFrame);
|
|
1262
|
+
await waitForFrame("terminal-operator-abort");
|
|
1263
|
+
expect(seamCalls).toEqual([
|
|
1264
|
+
{ handle: "exact-run-handle", scope: "turn" },
|
|
1265
|
+
{ handle: "later-run-handle", scope: "owned" },
|
|
1266
|
+
]);
|
|
1267
|
+
expect(transport.sent.find(frame => frame.id === "terminal-operator-abort")).toMatchObject({
|
|
1268
|
+
ok: true,
|
|
1269
|
+
result: expect.objectContaining({ turn: "stopped", ownedWork: "stopped" }),
|
|
1270
|
+
});
|
|
1271
|
+
const operatorKeyHash = createHash("sha256").update("terminal-operator-key").digest("hex");
|
|
1272
|
+
const deliveryDeadline = Date.now() + 15_000;
|
|
1273
|
+
while (
|
|
1274
|
+
reconciliationStore.snapshotTerminalScopes().find(record => record.idempotencyKeyHash === operatorKeyHash)
|
|
1275
|
+
?.responseState !== "sent"
|
|
1276
|
+
) {
|
|
1277
|
+
if (Date.now() > deliveryDeadline) throw new Error("Timed out waiting for operator delivery receipt");
|
|
1278
|
+
await Bun.sleep(20);
|
|
1279
|
+
}
|
|
1143
1280
|
} finally {
|
|
1281
|
+
setBrokerRuntimeAbortCapabilityForTest(undefined);
|
|
1144
1282
|
await handlers.get("session_shutdown")?.({}, ctx);
|
|
1145
1283
|
await rm(cwd, { recursive: true, force: true });
|
|
1146
1284
|
}
|
|
@@ -1351,7 +1489,71 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1351
1489
|
}
|
|
1352
1490
|
});
|
|
1353
1491
|
|
|
1354
|
-
test("SDK-only
|
|
1492
|
+
test("SDK-only operator abort never adopts a turn that starts after idle admission", async () => {
|
|
1493
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-operator-idle-race-"));
|
|
1494
|
+
const capability = "operator-idle-race-capability";
|
|
1495
|
+
setBrokerRuntimeAbortCapabilityForTest(capability);
|
|
1496
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1497
|
+
const api = {
|
|
1498
|
+
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
1499
|
+
handlers.set(event, handler);
|
|
1500
|
+
},
|
|
1501
|
+
} as unknown as ExtensionAPI;
|
|
1502
|
+
const transport = memoryTransport();
|
|
1503
|
+
const reconciliationStore = createReconciliationStore({
|
|
1504
|
+
sessionFile: path.join(cwd, "session.json"),
|
|
1505
|
+
sessionId: transport.sessionId,
|
|
1506
|
+
});
|
|
1507
|
+
const seamCalls: string[] = [];
|
|
1508
|
+
let promptReads = 0;
|
|
1509
|
+
createSdkSessionRuntimeExtension(api, {
|
|
1510
|
+
agentDir: cwd,
|
|
1511
|
+
createTransport: async () => transport,
|
|
1512
|
+
terminalAbortSeams: {
|
|
1513
|
+
getReconciliationStore: () => reconciliationStore,
|
|
1514
|
+
getTerminalTurnEpoch: () => (promptReads++ === 0 ? undefined : 9),
|
|
1515
|
+
getActivePromptHandle: () => (promptReads === 0 ? undefined : "successor-handle"),
|
|
1516
|
+
getActivePromptOwnerConnectionId: () => "successor-owner",
|
|
1517
|
+
cancelPendingPreflightForTerminalAbort: () => {},
|
|
1518
|
+
abortPromptAndWaitWithTerminal: async handle => {
|
|
1519
|
+
seamCalls.push(handle);
|
|
1520
|
+
return { status: "settled", terminalScope: {} };
|
|
1521
|
+
},
|
|
1522
|
+
},
|
|
1523
|
+
});
|
|
1524
|
+
const ctx = extensionContext(transport.sessionId, cwd);
|
|
1525
|
+
try {
|
|
1526
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
1527
|
+
transport.feed("local-operator", {
|
|
1528
|
+
type: "control_request",
|
|
1529
|
+
id: "operator-idle-race",
|
|
1530
|
+
operation: "turn.abort",
|
|
1531
|
+
input: {
|
|
1532
|
+
mode: "terminal",
|
|
1533
|
+
operator: true,
|
|
1534
|
+
[BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: capability,
|
|
1535
|
+
},
|
|
1536
|
+
confirm: true,
|
|
1537
|
+
idempotencyKey: "operator-idle-race-key",
|
|
1538
|
+
} as SdkFrame);
|
|
1539
|
+
const deadline = Date.now() + 15_000;
|
|
1540
|
+
while (!transport.sent.some(frame => frame.id === "operator-idle-race")) {
|
|
1541
|
+
if (Date.now() > deadline) throw new Error("Timed out waiting for operator idle-race response");
|
|
1542
|
+
await Bun.sleep(20);
|
|
1543
|
+
}
|
|
1544
|
+
expect(transport.sent.find(frame => frame.id === "operator-idle-race")).toMatchObject({
|
|
1545
|
+
ok: true,
|
|
1546
|
+
result: expect.objectContaining({ turn: "no_active_turn", terminal: "terminal_no_effect" }),
|
|
1547
|
+
});
|
|
1548
|
+
expect(seamCalls).toHaveLength(0);
|
|
1549
|
+
} finally {
|
|
1550
|
+
setBrokerRuntimeAbortCapabilityForTest(undefined);
|
|
1551
|
+
await handlers.get("session_shutdown")?.({}, ctx);
|
|
1552
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
test("SDK-only host re-reads the active prompt AND its owner after an owner-mismatch reservation race", async () => {
|
|
1355
1557
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-owner-race-"));
|
|
1356
1558
|
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1357
1559
|
const api = {
|
|
@@ -3172,6 +3374,7 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
3172
3374
|
},
|
|
3173
3375
|
} as any;
|
|
3174
3376
|
const sessionId = "broker-recovery";
|
|
3377
|
+
const endpointUrl = "ws://127.0.0.1:1";
|
|
3175
3378
|
createSdkSessionRuntimeExtension(api, {
|
|
3176
3379
|
agentDir,
|
|
3177
3380
|
createTransport: async ({ stateRoot, token }) => ({
|
|
@@ -3183,8 +3386,8 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
3183
3386
|
start: async () => {
|
|
3184
3387
|
const endpoint = path.join(stateRoot, "sdk", `${sessionId}.json`);
|
|
3185
3388
|
await mkdir(path.dirname(endpoint), { recursive: true });
|
|
3186
|
-
await writeFile(endpoint, JSON.stringify({ sessionId, token, pid: process.pid }));
|
|
3187
|
-
return { url:
|
|
3389
|
+
await writeFile(endpoint, JSON.stringify({ sessionId, token, pid: process.pid, url: endpointUrl }));
|
|
3390
|
+
return { url: endpointUrl };
|
|
3188
3391
|
},
|
|
3189
3392
|
stop: async () => {},
|
|
3190
3393
|
}),
|
|
@@ -3241,6 +3444,7 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
3241
3444
|
createSdkSessionRuntimeExtension(api, {
|
|
3242
3445
|
agentDir,
|
|
3243
3446
|
brokerRegistrationRequired: true,
|
|
3447
|
+
lifecycleRequestId: "broker-required-marker",
|
|
3244
3448
|
createTransport: async ({ sessionId, stateRoot, token }) => ({
|
|
3245
3449
|
sessionId,
|
|
3246
3450
|
stateRoot,
|