@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
|
@@ -37,8 +37,15 @@ import {
|
|
|
37
37
|
settleOwnedWork,
|
|
38
38
|
} from "../../session/terminal-abort";
|
|
39
39
|
import { parseThinkingLevel } from "../../thinking";
|
|
40
|
+
import { readEndpointFile } from "../broker/endpoint-authority";
|
|
40
41
|
import { ensureBroker } from "../broker/ensure";
|
|
41
|
-
import {
|
|
42
|
+
import { processIncarnation } from "../broker/process-incarnation";
|
|
43
|
+
import {
|
|
44
|
+
type MasterRoleAttestationV2,
|
|
45
|
+
resolveSessionLocator,
|
|
46
|
+
SessionIndex,
|
|
47
|
+
type SessionLocatorV2,
|
|
48
|
+
} from "../broker/session-index";
|
|
42
49
|
import {
|
|
43
50
|
parseSyntheticModelId,
|
|
44
51
|
resolveSyntheticModelSelection,
|
|
@@ -58,8 +65,12 @@ import {
|
|
|
58
65
|
resolveReconciliationSessionFile,
|
|
59
66
|
} from "../reconciliation-extensions";
|
|
60
67
|
import { sanitizeTurnResultContent, type TurnResultContent } from "../turn-result";
|
|
61
|
-
import { type ControlSurface, controlRequestFromFrame, dispatchControl } from "./control";
|
|
62
|
-
import {
|
|
68
|
+
import { type ControlSurface, controlRequestFromFrame, dispatchControl, terminalAbortIdentity } from "./control";
|
|
69
|
+
import {
|
|
70
|
+
BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
|
|
71
|
+
BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD,
|
|
72
|
+
hasBrokerRuntimeAbortCapability,
|
|
73
|
+
} from "./control/runtime-gate";
|
|
63
74
|
import { SessionSdkHost, type SessionSdkHostOptions } from "./host";
|
|
64
75
|
import { clearAutoroutingInactive, isAutoroutingInactive, markAutoroutingInactive } from "./internal-autorouting-state";
|
|
65
76
|
import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
|
|
@@ -103,6 +114,65 @@ const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
|
|
|
103
114
|
* after this bound are abandoned — their durable broker state is the recovery
|
|
104
115
|
* authority, and outcomes are inherently uncertain. */
|
|
105
116
|
const GATE_RESOLUTION_QUIESCENCE_MS = 5_000;
|
|
117
|
+
/** Master verification fields are ephemeral protocol values, not unbounded input. */
|
|
118
|
+
const MASTER_AUTH_VALUE_MAX_LENGTH = 512;
|
|
119
|
+
const MASTER_AUTH_VALUE_PATTERN = /^[A-Za-z0-9_-]+$/u;
|
|
120
|
+
/** Keep replay protection finite even when an authenticated client floods nonces. */
|
|
121
|
+
const MASTER_NONCE_REPLAY_MAX_ENTRIES = 1_024;
|
|
122
|
+
/** Verification exchanges complete in seconds; retain replay evidence for a bounded window. */
|
|
123
|
+
const MASTER_NONCE_REPLAY_TTL_MS = 5 * 60_000;
|
|
124
|
+
|
|
125
|
+
export type MasterCapabilityReplayState = Map<string, number>;
|
|
126
|
+
|
|
127
|
+
export function verifyMasterCapabilityFrame(input: {
|
|
128
|
+
frame: { nonce: unknown; attestationEpoch: unknown; capability: unknown };
|
|
129
|
+
expectedCapability: string | undefined;
|
|
130
|
+
expectedEpoch: string | undefined;
|
|
131
|
+
replay: MasterCapabilityReplayState;
|
|
132
|
+
now?: number;
|
|
133
|
+
}): { ok: boolean; nonce: string; attestationEpoch: string } {
|
|
134
|
+
const now = input.now ?? Date.now();
|
|
135
|
+
for (const [consumedNonce, expiresAt] of input.replay) {
|
|
136
|
+
if (expiresAt <= now) input.replay.delete(consumedNonce);
|
|
137
|
+
}
|
|
138
|
+
const bounded = (value: unknown): value is string =>
|
|
139
|
+
typeof value === "string" &&
|
|
140
|
+
value.length > 0 &&
|
|
141
|
+
value.length <= MASTER_AUTH_VALUE_MAX_LENGTH &&
|
|
142
|
+
MASTER_AUTH_VALUE_PATTERN.test(value);
|
|
143
|
+
const nonce = bounded(input.frame.nonce) ? input.frame.nonce : "";
|
|
144
|
+
const epoch = bounded(input.frame.attestationEpoch) ? input.frame.attestationEpoch : "";
|
|
145
|
+
const capability = bounded(input.frame.capability) ? input.frame.capability : "";
|
|
146
|
+
const expectedCapability = bounded(input.expectedCapability) ? input.expectedCapability : "";
|
|
147
|
+
const expectedEpoch = bounded(input.expectedEpoch) ? input.expectedEpoch : "";
|
|
148
|
+
const authenticatedFields =
|
|
149
|
+
bounded(input.frame.nonce) &&
|
|
150
|
+
bounded(input.frame.attestationEpoch) &&
|
|
151
|
+
bounded(input.frame.capability) &&
|
|
152
|
+
bounded(input.expectedCapability) &&
|
|
153
|
+
bounded(input.expectedEpoch) &&
|
|
154
|
+
epoch === expectedEpoch;
|
|
155
|
+
const expectedBytes = Buffer.from(expectedCapability);
|
|
156
|
+
const capabilityBytes = Buffer.from(capability);
|
|
157
|
+
const capabilityMatches =
|
|
158
|
+
authenticatedFields &&
|
|
159
|
+
expectedBytes.length === capabilityBytes.length &&
|
|
160
|
+
crypto.timingSafeEqual(expectedBytes, capabilityBytes);
|
|
161
|
+
const reusable = capabilityMatches && !input.replay.has(nonce);
|
|
162
|
+
if (reusable) {
|
|
163
|
+
if (input.replay.size >= MASTER_NONCE_REPLAY_MAX_ENTRIES) {
|
|
164
|
+
const oldest = input.replay.keys().next().value;
|
|
165
|
+
if (typeof oldest === "string") input.replay.delete(oldest);
|
|
166
|
+
}
|
|
167
|
+
input.replay.set(nonce, now + MASTER_NONCE_REPLAY_TTL_MS);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: reusable,
|
|
171
|
+
nonce,
|
|
172
|
+
attestationEpoch: authenticatedFields ? epoch : "",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
106
176
|
/** Maximum time a replaced runtime may retain a lifecycle persistence task. */
|
|
107
177
|
const LIFECYCLE_QUIESCENCE_MS = 1_000;
|
|
108
178
|
|
|
@@ -142,6 +212,8 @@ export interface SessionSdkRuntimeOptions
|
|
|
142
212
|
settings?: Settings;
|
|
143
213
|
/** Mutable shadow of patched config values merged into query readback. */
|
|
144
214
|
configOverrides?: Map<string, unknown>;
|
|
215
|
+
/** Stable master lineage identity retained across session switches/branches. */
|
|
216
|
+
masterOwnerSessionId?: string;
|
|
145
217
|
}
|
|
146
218
|
|
|
147
219
|
export interface SdkOnlyInvocationRecord extends InvocationCorrelation {
|
|
@@ -400,6 +472,8 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
400
472
|
agentDir: string;
|
|
401
473
|
/** Lifecycle-owned sessions require broker publication before they become usable. */
|
|
402
474
|
brokerRegistrationRequired?: boolean;
|
|
475
|
+
/** Trusted broker-issued lifecycle marker bound to lifecycle host index events. */
|
|
476
|
+
lifecycleRequestId?: string;
|
|
403
477
|
createTransport(input: {
|
|
404
478
|
sessionId: string;
|
|
405
479
|
stateRoot: string;
|
|
@@ -411,6 +485,12 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
411
485
|
onSdkRequest?: SessionSdkHostOptions["onRequest"];
|
|
412
486
|
/** Mutable shadow of patched config values merged into query readback. */
|
|
413
487
|
configOverrides?: Map<string, unknown>;
|
|
488
|
+
/** In-memory master capability for private broker verification only. */
|
|
489
|
+
masterCapability?: string;
|
|
490
|
+
/** Opaque direct-role epoch this effective host may adopt. */
|
|
491
|
+
masterAttestationEpoch?: string;
|
|
492
|
+
/** Stable master lineage identity retained across session switches/branches. */
|
|
493
|
+
masterOwnerSessionId?: string;
|
|
414
494
|
/** Private session-owned terminal-abort capabilities; never exposed on ExtensionContext. */
|
|
415
495
|
terminalAbortSeams?: SdkOnlyTerminalAbortSeams;
|
|
416
496
|
/** Callback when a frame is admitted to the runtime (test harness). */
|
|
@@ -1117,18 +1197,15 @@ function createQuerySurface(
|
|
|
1117
1197
|
kind: ctx.sessionMetadata?.kind ?? "main",
|
|
1118
1198
|
});
|
|
1119
1199
|
const lastAssistant = () => {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
.
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
)
|
|
1130
|
-
.map(block => block.text)
|
|
1131
|
-
.join("");
|
|
1200
|
+
const transcript =
|
|
1201
|
+
typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : [];
|
|
1202
|
+
for (const entry of transcript.toReversed()) {
|
|
1203
|
+
if (entry.role !== "assistant") continue;
|
|
1204
|
+
const text =
|
|
1205
|
+
typeof entry.body === "string"
|
|
1206
|
+
? entry.body
|
|
1207
|
+
: entry.content?.flatMap(block => (block.type === "text" ? [block.text] : [])).join("\n");
|
|
1208
|
+
if (text !== undefined && text.trim().length > 0) return text;
|
|
1132
1209
|
}
|
|
1133
1210
|
return undefined;
|
|
1134
1211
|
};
|
|
@@ -2269,7 +2346,7 @@ function createControlSurface(
|
|
|
2269
2346
|
}
|
|
2270
2347
|
};
|
|
2271
2348
|
const terminalAbort = async (
|
|
2272
|
-
input: { mode: "terminal"; scope?: "turn" | "owned" },
|
|
2349
|
+
input: { mode: "terminal"; scope?: "turn" | "owned"; operator?: boolean },
|
|
2273
2350
|
idempotencyKey?: string,
|
|
2274
2351
|
): Promise<unknown> => {
|
|
2275
2352
|
const scope = input.scope === "owned" ? "owned" : "turn";
|
|
@@ -2300,10 +2377,12 @@ function createControlSurface(
|
|
|
2300
2377
|
typeof idempotencyKey === "string"
|
|
2301
2378
|
? crypto.createHash("sha256").update(idempotencyKey).digest("hex")
|
|
2302
2379
|
: undefined;
|
|
2303
|
-
const
|
|
2304
|
-
|
|
2305
|
-
.
|
|
2306
|
-
|
|
2380
|
+
const abortIdentity = terminalAbortIdentity(
|
|
2381
|
+
{ mode: "terminal", scope, ...(input.operator === true ? { operator: true } : {}) },
|
|
2382
|
+
input.operator === true,
|
|
2383
|
+
);
|
|
2384
|
+
if (!abortIdentity) throw new Error("Terminal abort identity is invalid after dispatch validation.");
|
|
2385
|
+
const inputHash = abortIdentity.inputHash;
|
|
2307
2386
|
const stored = (record: SdkOnlyTerminalScopeRecord | SdkOnlyEvictedTerminalKeyEntry) => ({
|
|
2308
2387
|
responseState: record.responseState ?? "pending",
|
|
2309
2388
|
responsePayloadHash: record.responsePayloadHash ?? inputHash,
|
|
@@ -2676,6 +2755,16 @@ function createControlSurface(
|
|
|
2676
2755
|
pendingPreflights.delete(requesterBucketKey);
|
|
2677
2756
|
}
|
|
2678
2757
|
};
|
|
2758
|
+
const returnNoActiveTurn = async () => {
|
|
2759
|
+
const result = {
|
|
2760
|
+
ok: true,
|
|
2761
|
+
selection: scope,
|
|
2762
|
+
turn: "no_active_turn",
|
|
2763
|
+
terminal: "terminal_no_effect",
|
|
2764
|
+
};
|
|
2765
|
+
await finalizeNoEffectReservation(result);
|
|
2766
|
+
return result;
|
|
2767
|
+
};
|
|
2679
2768
|
if (!handle || epoch === undefined) {
|
|
2680
2769
|
if ((await writeNoEffect()) === "conflict") {
|
|
2681
2770
|
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
@@ -2691,6 +2780,10 @@ function createControlSurface(
|
|
|
2691
2780
|
if (replayed !== undefined) return replayed;
|
|
2692
2781
|
}
|
|
2693
2782
|
cancelRequesterPreflights();
|
|
2783
|
+
// Operator authority overrides connection ownership only for a turn
|
|
2784
|
+
// observed at admission. It must never adopt a successor that starts
|
|
2785
|
+
// while the durable no-effect reservation is being written.
|
|
2786
|
+
if (input.operator === true) return await returnNoActiveTurn();
|
|
2694
2787
|
// A prompt for this requester may have become ACTIVE while the
|
|
2695
2788
|
// reservation awaited the filesystem transaction: its submit()
|
|
2696
2789
|
// cleanup already removed the preflight callback, so cancelling here
|
|
@@ -2706,14 +2799,7 @@ function createControlSurface(
|
|
|
2706
2799
|
// No prompt won the race: finalize the reserved row so a later
|
|
2707
2800
|
// same-key retry replays this deterministic no_active_turn result
|
|
2708
2801
|
// (review thread P2).
|
|
2709
|
-
|
|
2710
|
-
ok: true,
|
|
2711
|
-
selection: scope,
|
|
2712
|
-
turn: "no_active_turn",
|
|
2713
|
-
terminal: "terminal_no_effect",
|
|
2714
|
-
};
|
|
2715
|
-
await finalizeNoEffectReservation(noActiveTurnResult);
|
|
2716
|
-
return noActiveTurnResult;
|
|
2802
|
+
return await returnNoActiveTurn();
|
|
2717
2803
|
}
|
|
2718
2804
|
handle = recheckedHandle;
|
|
2719
2805
|
epoch = recheckedEpoch;
|
|
@@ -2743,7 +2829,11 @@ function createControlSurface(
|
|
|
2743
2829
|
// cleared after a terminal lifecycle boundary) authorizes no client, and
|
|
2744
2830
|
// a stale prior owner authorizes only that old client — never a later
|
|
2745
2831
|
// turn it did not submit (review thread P1).
|
|
2746
|
-
if (
|
|
2832
|
+
if (
|
|
2833
|
+
input.operator !== true &&
|
|
2834
|
+
handle &&
|
|
2835
|
+
(abortingConnectionId === undefined || !currentOwnerConnectionIds().has(abortingConnectionId))
|
|
2836
|
+
) {
|
|
2747
2837
|
if ((await writeNoEffect()) === "conflict") {
|
|
2748
2838
|
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
2749
2839
|
code: "idempotency_conflict",
|
|
@@ -3412,6 +3502,107 @@ export function registerSdkOnlyNotificationCommand(api: ExtensionAPI): void {
|
|
|
3412
3502
|
});
|
|
3413
3503
|
}
|
|
3414
3504
|
|
|
3505
|
+
export function masterAttestationForEffectiveHost(input: {
|
|
3506
|
+
masterCapability: string | undefined;
|
|
3507
|
+
attestationEpoch: string | undefined;
|
|
3508
|
+
ownerSessionId?: string;
|
|
3509
|
+
sessionId: string;
|
|
3510
|
+
pid: number;
|
|
3511
|
+
processIncarnation: string | undefined;
|
|
3512
|
+
direct: MasterRoleAttestationV2 | undefined;
|
|
3513
|
+
}): MasterRoleAttestationV2 | undefined {
|
|
3514
|
+
const direct = input.direct;
|
|
3515
|
+
if (
|
|
3516
|
+
input.masterCapability === undefined ||
|
|
3517
|
+
input.attestationEpoch === undefined ||
|
|
3518
|
+
direct === undefined ||
|
|
3519
|
+
direct.ownerSessionId !== (input.ownerSessionId ?? input.sessionId) ||
|
|
3520
|
+
direct.launchPid !== input.pid ||
|
|
3521
|
+
direct.launchProcessIncarnation !== input.processIncarnation ||
|
|
3522
|
+
direct.attestationEpoch !== input.attestationEpoch
|
|
3523
|
+
)
|
|
3524
|
+
return undefined;
|
|
3525
|
+
return direct;
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
function masterDirectAttestation(input: {
|
|
3529
|
+
masterCapability: string | undefined;
|
|
3530
|
+
attestationEpoch: string | undefined;
|
|
3531
|
+
ownerSessionId?: string;
|
|
3532
|
+
sessionId: string;
|
|
3533
|
+
pid: number;
|
|
3534
|
+
processIncarnation: string | undefined;
|
|
3535
|
+
}): MasterRoleAttestationV2 | undefined {
|
|
3536
|
+
if (
|
|
3537
|
+
input.masterCapability === undefined ||
|
|
3538
|
+
input.attestationEpoch === undefined ||
|
|
3539
|
+
input.processIncarnation === undefined
|
|
3540
|
+
)
|
|
3541
|
+
return undefined;
|
|
3542
|
+
return {
|
|
3543
|
+
version: 2,
|
|
3544
|
+
ownerSessionId: input.ownerSessionId ?? input.sessionId,
|
|
3545
|
+
launchPid: input.pid,
|
|
3546
|
+
launchProcessIncarnation: input.processIncarnation,
|
|
3547
|
+
role: "master",
|
|
3548
|
+
attestationEpoch: input.attestationEpoch,
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
function sameMasterAttestation(left: MasterRoleAttestationV2, right: MasterRoleAttestationV2): boolean {
|
|
3553
|
+
return (
|
|
3554
|
+
left.version === right.version &&
|
|
3555
|
+
left.ownerSessionId === right.ownerSessionId &&
|
|
3556
|
+
left.launchPid === right.launchPid &&
|
|
3557
|
+
left.launchProcessIncarnation === right.launchProcessIncarnation &&
|
|
3558
|
+
left.role === right.role &&
|
|
3559
|
+
left.attestationEpoch === right.attestationEpoch
|
|
3560
|
+
);
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
/**
|
|
3564
|
+
* Establish direct master authority for the session currently hosted by this
|
|
3565
|
+
* process. Session identity transitions retain the master capability and epoch
|
|
3566
|
+
* but require a new direct attestation before their effective endpoint can
|
|
3567
|
+
* adopt that authority.
|
|
3568
|
+
*/
|
|
3569
|
+
export async function reattestMasterSessionIdentity(input: {
|
|
3570
|
+
index: SessionIndex;
|
|
3571
|
+
locator: SessionLocatorV2;
|
|
3572
|
+
masterCapability: string | undefined;
|
|
3573
|
+
attestationEpoch: string | undefined;
|
|
3574
|
+
ownerSessionId?: string;
|
|
3575
|
+
sessionId: string;
|
|
3576
|
+
pid: number;
|
|
3577
|
+
processIncarnation: string | undefined;
|
|
3578
|
+
}): Promise<MasterRoleAttestationV2 | undefined> {
|
|
3579
|
+
const masterRole = masterDirectAttestation(input);
|
|
3580
|
+
if (!masterRole) return undefined;
|
|
3581
|
+
const directExists = input.index.listSessionIdentities().some(row => {
|
|
3582
|
+
const processIdentity = row.hostIncarnation ?? row.processIncarnation;
|
|
3583
|
+
return (
|
|
3584
|
+
row.sessionId === input.sessionId &&
|
|
3585
|
+
row.endpointGeneration === 0 &&
|
|
3586
|
+
row.pid === input.pid &&
|
|
3587
|
+
processIdentity === input.processIncarnation &&
|
|
3588
|
+
row.masterRole !== undefined &&
|
|
3589
|
+
sameMasterAttestation(row.masterRole, masterRole)
|
|
3590
|
+
);
|
|
3591
|
+
});
|
|
3592
|
+
if (!directExists) {
|
|
3593
|
+
await input.index.append({
|
|
3594
|
+
type: "host_registered",
|
|
3595
|
+
sessionId: input.sessionId,
|
|
3596
|
+
locator: input.locator,
|
|
3597
|
+
endpointGeneration: 0,
|
|
3598
|
+
pid: input.pid,
|
|
3599
|
+
processIncarnation: input.processIncarnation,
|
|
3600
|
+
masterRole,
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
return masterRole;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3415
3606
|
function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
3416
3607
|
const error = { code: "session_quiescing", message: "The session endpoint is being replaced." };
|
|
3417
3608
|
const id = typeof frame.id === "string" ? frame.id : "";
|
|
@@ -4172,6 +4363,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4172
4363
|
await current.registerBroker();
|
|
4173
4364
|
current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
|
|
4174
4365
|
});
|
|
4366
|
+
const consumedMasterNonces = new Map<string, number>();
|
|
4175
4367
|
api.on("turn_end", (_event, ctx) =>
|
|
4176
4368
|
lifecycleStateForContext(ctx, "agent_end")?.runtime.emitEvent({
|
|
4177
4369
|
type: "turn_end",
|
|
@@ -4731,6 +4923,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4731
4923
|
};
|
|
4732
4924
|
runtime = new SessionSdkSessionRuntime({
|
|
4733
4925
|
transport,
|
|
4926
|
+
masterCapabilityVerify: frame =>
|
|
4927
|
+
verifyMasterCapabilityFrame({
|
|
4928
|
+
frame,
|
|
4929
|
+
expectedCapability: options.masterCapability,
|
|
4930
|
+
expectedEpoch: options.masterAttestationEpoch,
|
|
4931
|
+
replay: consumedMasterNonces,
|
|
4932
|
+
}),
|
|
4734
4933
|
onFrameAdmission: (_connectionId, frame) =>
|
|
4735
4934
|
inputGate.quiescing ? quiescingFrame(frame as Record<string, unknown>) : undefined,
|
|
4736
4935
|
control: async (connectionId, frame) => {
|
|
@@ -4776,21 +4975,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4776
4975
|
(request.input as { mode?: unknown }).mode !== "terminal"
|
|
4777
4976
|
)
|
|
4778
4977
|
return;
|
|
4779
|
-
const
|
|
4780
|
-
const
|
|
4781
|
-
const
|
|
4782
|
-
|
|
4783
|
-
if (
|
|
4784
|
-
// A malformed retry (e.g. {mode:"terminal", scope:"turn", extra:true})
|
|
4785
|
-
// rejected by dispatch must never hash as the valid turn input and
|
|
4786
|
-
// advance the legitimate stored row (review thread P2).
|
|
4787
|
-
for (const key of Object.keys(input)) if (key !== "mode" && key !== "scope") return;
|
|
4788
|
-
const scopeInput = rawScope === "owned" ? "owned" : "turn";
|
|
4978
|
+
const rawInput = request.input as Record<string, unknown>;
|
|
4979
|
+
const operatorAuthorized = hasBrokerRuntimeAbortCapability(rawInput);
|
|
4980
|
+
const { [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: _capability, ...publicInput } = rawInput;
|
|
4981
|
+
const abortIdentity = terminalAbortIdentity(publicInput, operatorAuthorized);
|
|
4982
|
+
if (!abortIdentity) return;
|
|
4789
4983
|
const keyHash = crypto.createHash("sha256").update(String(request.idempotencyKey)).digest("hex");
|
|
4790
|
-
const inputHash =
|
|
4791
|
-
.createHash("sha256")
|
|
4792
|
-
.update(JSON.stringify({ mode: "terminal", scope: scopeInput }))
|
|
4793
|
-
.digest("hex");
|
|
4984
|
+
const inputHash = abortIdentity.inputHash;
|
|
4794
4985
|
// Hash the ACTUAL written response payload: the durable state may only
|
|
4795
4986
|
// advance when the written response corresponds to the row's payload.
|
|
4796
4987
|
// When more than 256 concurrent requests evict an in-flight abort from
|
|
@@ -4846,21 +5037,72 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4846
5037
|
const disposeGate = ctx.workflowGate?.onGateEmitted?.(gate =>
|
|
4847
5038
|
runtime.emitEvent({ kind: "workflow_gate", payload: gate }),
|
|
4848
5039
|
);
|
|
5040
|
+
let publishedEndpointUrl: string | undefined;
|
|
4849
5041
|
let brokerRegistered = false;
|
|
4850
5042
|
const registerBroker = async (): Promise<void> => {
|
|
4851
5043
|
if (brokerRegistered) return;
|
|
4852
5044
|
try {
|
|
5045
|
+
if (options.brokerRegistrationRequired && !options.lifecycleRequestId)
|
|
5046
|
+
throw new Error("Lifecycle broker registration requires a request identity.");
|
|
4853
5047
|
await ensureBroker({ agentDir: options.agentDir });
|
|
4854
5048
|
const index = await new SessionIndex(options.agentDir).open();
|
|
4855
|
-
const locator =
|
|
5049
|
+
const locator = await resolveSessionLocator(ctx.cwd, stateRoot);
|
|
5050
|
+
const effectiveIncarnation = processIncarnation(process.pid);
|
|
5051
|
+
const direct = await reattestMasterSessionIdentity({
|
|
5052
|
+
index,
|
|
5053
|
+
locator,
|
|
5054
|
+
masterCapability: options.masterCapability,
|
|
5055
|
+
attestationEpoch: options.masterAttestationEpoch,
|
|
5056
|
+
ownerSessionId: options.masterOwnerSessionId,
|
|
5057
|
+
sessionId,
|
|
5058
|
+
pid: process.pid,
|
|
5059
|
+
processIncarnation: effectiveIncarnation,
|
|
5060
|
+
});
|
|
4856
5061
|
await runtime.registerWithBroker({
|
|
4857
5062
|
register: async input => {
|
|
4858
|
-
|
|
4859
|
-
.
|
|
4860
|
-
|
|
5063
|
+
if (publishedEndpointUrl === undefined)
|
|
5064
|
+
throw new Error("SDK transport endpoint was not published before broker registration.");
|
|
5065
|
+
const endpointPath = path.join(input.stateRoot, "sdk", `${input.sessionId}.json`);
|
|
5066
|
+
const file = await readEndpointFile(endpointPath);
|
|
5067
|
+
if (!file) throw new Error("SDK endpoint could not be read as a stable regular file.");
|
|
5068
|
+
const endpoint = JSON.parse(file.source) as Record<string, unknown>;
|
|
5069
|
+
if (
|
|
5070
|
+
endpoint.sessionId !== input.sessionId ||
|
|
5071
|
+
endpoint.pid !== process.pid ||
|
|
5072
|
+
endpoint.url !== publishedEndpointUrl ||
|
|
5073
|
+
endpoint.token !== transport.token
|
|
5074
|
+
)
|
|
5075
|
+
throw new Error("SDK endpoint did not match the published transport authority.");
|
|
5076
|
+
const endpointMtimeMs = file.mtimeMs;
|
|
5077
|
+
const endpointFileId = `${file.dev}:${file.ino}`;
|
|
5078
|
+
const masterRole = masterAttestationForEffectiveHost({
|
|
5079
|
+
masterCapability: options.masterCapability,
|
|
5080
|
+
attestationEpoch: options.masterAttestationEpoch,
|
|
5081
|
+
ownerSessionId: options.masterOwnerSessionId,
|
|
5082
|
+
sessionId: input.sessionId,
|
|
5083
|
+
pid: process.pid,
|
|
5084
|
+
processIncarnation: effectiveIncarnation,
|
|
5085
|
+
direct,
|
|
5086
|
+
});
|
|
5087
|
+
await index.append({
|
|
5088
|
+
type: "host_registered",
|
|
5089
|
+
...input,
|
|
5090
|
+
locator,
|
|
5091
|
+
pid: process.pid,
|
|
5092
|
+
endpointMtimeMs,
|
|
5093
|
+
endpointFileId,
|
|
5094
|
+
...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
|
|
5095
|
+
...(masterRole ? { masterRole } : {}),
|
|
5096
|
+
});
|
|
4861
5097
|
},
|
|
4862
5098
|
unregister: async input => {
|
|
4863
|
-
await index.append({
|
|
5099
|
+
await index.append({
|
|
5100
|
+
type: "host_unregistered",
|
|
5101
|
+
...input,
|
|
5102
|
+
locator,
|
|
5103
|
+
pid: process.pid,
|
|
5104
|
+
...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
|
|
5105
|
+
});
|
|
4864
5106
|
},
|
|
4865
5107
|
});
|
|
4866
5108
|
brokerRegistered = true;
|
|
@@ -4899,7 +5141,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4899
5141
|
lifecycleOwnerHolder.state = runtimeOwner;
|
|
4900
5142
|
active = runtimeOwner;
|
|
4901
5143
|
try {
|
|
4902
|
-
await runtime.start();
|
|
5144
|
+
publishedEndpointUrl = (await runtime.start()).url;
|
|
4903
5145
|
await registerBroker();
|
|
4904
5146
|
} catch (error) {
|
|
4905
5147
|
active = undefined;
|
|
@@ -51,3 +51,31 @@ export class AgentDirSessionLifecycleClient implements SessionLifecycleClient {
|
|
|
51
51
|
export function createBrokerSessionLifecycleService(agentDir: string): SessionLifecycleService {
|
|
52
52
|
return new SessionLifecycleService(new AgentDirSessionLifecycleClient(agentDir));
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Dedicated dispatch for the local master `session.spawn` surface. It shares
|
|
57
|
+
* the lifecycle client's credential boundary but bypasses the generic mutation
|
|
58
|
+
* outcome types: spawn responses are validated by the caller's allowlist.
|
|
59
|
+
*/
|
|
60
|
+
export async function dispatchSpawnGlobal(
|
|
61
|
+
agentDir: string,
|
|
62
|
+
input: Record<string, unknown>,
|
|
63
|
+
idempotencyKey: string,
|
|
64
|
+
timeoutMs: number,
|
|
65
|
+
): Promise<unknown> {
|
|
66
|
+
await ensureBroker({ agentDir });
|
|
67
|
+
const discovery = await readSdkBrokerDiscovery(agentDir);
|
|
68
|
+
if (!discovery) throw new Error("SDK broker discovery is unavailable.");
|
|
69
|
+
const client = await SdkClient.connect(discovery.url, discovery.token, { timeoutMs });
|
|
70
|
+
try {
|
|
71
|
+
return await client.global("session.spawn", input, { idempotencyKey, timeoutMs });
|
|
72
|
+
} finally {
|
|
73
|
+
try {
|
|
74
|
+
await client.close();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logger.warn("SDK spawn client cleanup failed", {
|
|
77
|
+
error: error instanceof Error ? error.message : String(error),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|