@gajae-code/coding-agent 0.15.6 → 0.16.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 +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -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 +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- 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/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -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/main.d.ts +1 -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/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- 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/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -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/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -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/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- 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 +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- 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/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- 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 +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- 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/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- 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 +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- 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 +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- 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/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- 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 +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -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 +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- 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 +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- 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/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
import { validateRequiredPromptText } from "../../protocol/adapter-validation";
|
|
7
7
|
import { OPERATIONS, type Operation } from "../../protocol/operation-registry";
|
|
8
8
|
import type { ControlInput, ControlSurface, ControlValue } from "./operations";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
|
|
11
|
+
brokerRuntimeCloseCapability,
|
|
12
|
+
hasBrokerRuntimeAbortCapability,
|
|
13
|
+
hasBrokerRuntimeCloseCapability,
|
|
14
|
+
} from "./runtime-gate";
|
|
10
15
|
|
|
11
16
|
export interface ControlRequest {
|
|
12
17
|
id: string;
|
|
@@ -149,29 +154,35 @@ function inputHash(input: unknown): string {
|
|
|
149
154
|
.update(JSON.stringify(canonicalize(input)))
|
|
150
155
|
.digest("hex");
|
|
151
156
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
157
|
+
|
|
158
|
+
export interface TerminalAbortIdentity {
|
|
159
|
+
input: { mode: "terminal"; scope: "turn" | "owned"; operator?: true };
|
|
160
|
+
inputHash: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** One strict, capability-free identity shared by dispatch, durable admission, and delivery observation. */
|
|
164
|
+
export function terminalAbortIdentity(input: unknown, operatorAuthorized: boolean): TerminalAbortIdentity | undefined {
|
|
165
|
+
if (!isInput(input) || input.mode !== "terminal") return undefined;
|
|
166
|
+
for (const key of Object.keys(input)) if (!TERMINAL_ABORT_FIELDS.has(key)) return undefined;
|
|
167
|
+
let scope: "turn" | "owned";
|
|
168
|
+
if (input.scope === undefined || input.scope === "turn") scope = "turn";
|
|
169
|
+
else if (input.scope === "owned") scope = "owned";
|
|
170
|
+
else return undefined;
|
|
171
|
+
if (input.operator !== undefined && input.operator !== true) return undefined;
|
|
172
|
+
if (input.operator === true && !operatorAuthorized) return undefined;
|
|
173
|
+
const normalized = {
|
|
174
|
+
mode: "terminal" as const,
|
|
175
|
+
scope,
|
|
176
|
+
...(input.operator === true ? { operator: true as const } : {}),
|
|
177
|
+
};
|
|
178
|
+
return { input: normalized, inputHash: inputHash(normalized) };
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
function text(input: ControlInput, key = "text"): string {
|
|
171
182
|
return input[key] as string;
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
const TERMINAL_ABORT_FIELDS = new Set(["mode", "scope"]);
|
|
185
|
+
const TERMINAL_ABORT_FIELDS = new Set(["mode", "scope", "operator"]);
|
|
175
186
|
|
|
176
187
|
function invalidInput(message: string): never {
|
|
177
188
|
throw new TypedControlError("invalid_input", message);
|
|
@@ -184,15 +195,23 @@ function invalidInput(message: string): never {
|
|
|
184
195
|
* input is dropped and the ordinary argument-less `surface.abort()` runs.
|
|
185
196
|
*
|
|
186
197
|
* Terminal mode (`mode:"terminal"`) is validated strictly and side-effect-free
|
|
187
|
-
* before any surface call: only `mode`/`scope` fields are accepted,
|
|
188
|
-
* must be `"turn"` or `"owned"` (default `"turn"`), and a nonempty
|
|
189
|
-
* key of at most 128 UTF-8 bytes is required on the request envelope.
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
198
|
+
* before any surface call: only `mode`/`scope`/`operator` fields are accepted,
|
|
199
|
+
* `scope` must be `"turn"` or `"owned"` (default `"turn"`), and a nonempty
|
|
200
|
+
* idempotency key of at most 128 UTF-8 bytes is required on the request envelope.
|
|
201
|
+
* A cross-connection local operator must arrive through the Broker route with a
|
|
202
|
+
* private lifecycle capability, set `operator:true`, and confirm the destructive
|
|
203
|
+
* control request explicitly. Terminal semantics (see the approved plan) always
|
|
204
|
+
* stop the root worker's current turn. `scope:"turn"` leaves owned work running
|
|
205
|
+
* so its completion can resume the root worker; `scope:"owned"` additionally
|
|
206
|
+
* requires exact causal proof and stops that owned work. Operator authority
|
|
207
|
+
* changes only connection ownership, never those settlement proofs.
|
|
194
208
|
*/
|
|
195
|
-
function invokeAbort(
|
|
209
|
+
function invokeAbort(
|
|
210
|
+
surface: ControlSurface,
|
|
211
|
+
input: ControlInput,
|
|
212
|
+
confirm: boolean | undefined,
|
|
213
|
+
idempotencyKey: string | undefined,
|
|
214
|
+
): ControlValue {
|
|
196
215
|
const mode = input.mode === undefined ? "turn" : input.mode;
|
|
197
216
|
if (mode === "turn") return surface.abort();
|
|
198
217
|
if (mode !== "terminal") invalidInput('turn.abort mode must be "turn" or "terminal".');
|
|
@@ -200,12 +219,18 @@ function invokeAbort(surface: ControlSurface, input: ControlInput, idempotencyKe
|
|
|
200
219
|
if (!TERMINAL_ABORT_FIELDS.has(key)) invalidInput(`Unknown turn.abort terminal field: ${key}`);
|
|
201
220
|
const scope = input.scope === undefined ? "turn" : input.scope;
|
|
202
221
|
if (scope !== "turn" && scope !== "owned") invalidInput('turn.abort terminal scope must be "turn" or "owned".');
|
|
222
|
+
if (input.operator !== undefined && input.operator !== true)
|
|
223
|
+
invalidInput("turn.abort terminal operator must be true when provided.");
|
|
224
|
+
if (input.operator === true && confirm !== true) invalidInput("operator terminal abort requires confirm:true.");
|
|
203
225
|
if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0)
|
|
204
226
|
invalidInput("terminal abort requires a nonempty idempotency key.");
|
|
205
227
|
if (new TextEncoder().encode(idempotencyKey).length > 128)
|
|
206
228
|
invalidInput("terminal abort idempotency key must be at most 128 UTF-8 bytes.");
|
|
207
229
|
if (!surface.abortTerminal) invalidInput("terminal abort is not supported by this surface.");
|
|
208
|
-
return surface.abortTerminal(
|
|
230
|
+
return surface.abortTerminal(
|
|
231
|
+
{ mode: "terminal", scope, ...(input.operator === true ? { operator: true } : {}) },
|
|
232
|
+
idempotencyKey,
|
|
233
|
+
);
|
|
209
234
|
}
|
|
210
235
|
function invoke(
|
|
211
236
|
surface: ControlSurface,
|
|
@@ -222,7 +247,7 @@ function invoke(
|
|
|
222
247
|
case "turn.follow_up":
|
|
223
248
|
return surface.followUp(text(input));
|
|
224
249
|
case "turn.abort":
|
|
225
|
-
return invokeAbort(surface, input, idempotencyKey);
|
|
250
|
+
return invokeAbort(surface, input, confirm, idempotencyKey);
|
|
226
251
|
case "turn.abort_and_prompt":
|
|
227
252
|
return surface.abortAndPrompt(text(input));
|
|
228
253
|
case "ask.answer":
|
|
@@ -266,8 +291,8 @@ function invoke(
|
|
|
266
291
|
return surface.setQueueMode("steering", input.mode);
|
|
267
292
|
case "queue.follow_up_mode.set":
|
|
268
293
|
return surface.setQueueMode("follow_up", input.mode);
|
|
269
|
-
case "queue.
|
|
270
|
-
return surface.setQueueMode("
|
|
294
|
+
case "queue.tool_interrupt_policy.set":
|
|
295
|
+
return surface.setQueueMode("tool_interrupt", input.mode);
|
|
271
296
|
case "compaction.run":
|
|
272
297
|
return surface.runCompaction();
|
|
273
298
|
case "compaction.auto.set":
|
|
@@ -404,11 +429,7 @@ function idempotent(
|
|
|
404
429
|
const now = Date.now();
|
|
405
430
|
for (const [key, entry] of requests) if (entry.expiresAt <= now) requests.delete(key);
|
|
406
431
|
const key = `${row.sdkId}\u0000${request.idempotencyKey}`;
|
|
407
|
-
|
|
408
|
-
// defaulted and explicit shapes share one idempotency key (and reach the
|
|
409
|
-
// durable terminal-scope replay on eviction); malformed inputs stay raw.
|
|
410
|
-
const hashInput = row.sdkId === "turn.abort" ? normalizeTerminalAbortInputForHash(request.input) : request.input;
|
|
411
|
-
const hash = inputHash(hashInput);
|
|
432
|
+
const hash = inputHash(request.input);
|
|
412
433
|
const existing = requests.get(key);
|
|
413
434
|
if (existing) {
|
|
414
435
|
requests.delete(key);
|
|
@@ -445,6 +466,10 @@ export function dispatchControl(
|
|
|
445
466
|
failure(request.id, "unknown_operation", `Unknown control operation: ${request.operation}.`),
|
|
446
467
|
);
|
|
447
468
|
const brokerCloseAuthorized = row.sdkId === "session.close" && hasBrokerRuntimeCloseCapability(request.input);
|
|
469
|
+
const brokerAbortInput = row.sdkId === "turn.abort" && isInput(request.input) ? request.input : undefined;
|
|
470
|
+
const brokerAbortFieldPresent =
|
|
471
|
+
brokerAbortInput !== undefined && Object.hasOwn(brokerAbortInput, BROKER_RUNTIME_ABORT_CAPABILITY_FIELD);
|
|
472
|
+
const brokerAbortAuthorized = row.sdkId === "turn.abort" && hasBrokerRuntimeAbortCapability(request.input);
|
|
448
473
|
if (BROKER_LIFECYCLE_CONTROL_OPERATIONS.has(row.sdkId) && !brokerCloseAuthorized)
|
|
449
474
|
return Promise.resolve(
|
|
450
475
|
failure(
|
|
@@ -453,6 +478,20 @@ export function dispatchControl(
|
|
|
453
478
|
`${request.operation} is available only through the Broker lifecycle service.`,
|
|
454
479
|
),
|
|
455
480
|
);
|
|
481
|
+
if (!isInput(request.input))
|
|
482
|
+
return Promise.resolve(failure(request.id, "invalid_input", "Control input must be an object."));
|
|
483
|
+
if (
|
|
484
|
+
row.sdkId === "turn.abort" &&
|
|
485
|
+
((brokerAbortFieldPresent && !brokerAbortAuthorized) ||
|
|
486
|
+
(request.input.operator === true && !brokerAbortAuthorized))
|
|
487
|
+
)
|
|
488
|
+
return Promise.resolve(
|
|
489
|
+
failure(
|
|
490
|
+
request.id,
|
|
491
|
+
"operation_prohibited",
|
|
492
|
+
"Operator terminal aborts are available only through the Broker control route.",
|
|
493
|
+
),
|
|
494
|
+
);
|
|
456
495
|
if (
|
|
457
496
|
!brokerCloseAuthorized &&
|
|
458
497
|
surface.installedOperations instanceof Set &&
|
|
@@ -461,20 +500,42 @@ export function dispatchControl(
|
|
|
461
500
|
return Promise.resolve(
|
|
462
501
|
failure(request.id, "operation_not_session_owned", `${request.operation} is not installed for this session.`),
|
|
463
502
|
);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
503
|
+
let dispatchRequest: ControlRequest = request;
|
|
504
|
+
if (row.sdkId === "turn.abort" && request.input.mode === "terminal") {
|
|
505
|
+
const publicInput = brokerAbortInput
|
|
506
|
+
? (() => {
|
|
507
|
+
const { [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: _capability, ...input } = brokerAbortInput;
|
|
508
|
+
return input;
|
|
509
|
+
})()
|
|
510
|
+
: request.input;
|
|
511
|
+
const identity = terminalAbortIdentity(publicInput, brokerAbortAuthorized);
|
|
512
|
+
if (!identity)
|
|
513
|
+
return Promise.resolve(failure(request.id, "invalid_input", "Terminal turn.abort input is malformed."));
|
|
514
|
+
if (identity.input.operator === true && request.confirm !== true)
|
|
515
|
+
return Promise.resolve(failure(request.id, "invalid_input", "operator terminal abort requires confirm:true."));
|
|
516
|
+
if (typeof request.idempotencyKey !== "string" || request.idempotencyKey.length === 0)
|
|
517
|
+
return Promise.resolve(
|
|
518
|
+
failure(request.id, "invalid_input", "terminal abort requires a nonempty idempotency key."),
|
|
519
|
+
);
|
|
520
|
+
if (new TextEncoder().encode(request.idempotencyKey).length > 128)
|
|
521
|
+
return Promise.resolve(
|
|
522
|
+
failure(request.id, "invalid_input", "terminal abort idempotency key must be at most 128 UTF-8 bytes."),
|
|
523
|
+
);
|
|
524
|
+
dispatchRequest = { ...request, input: identity.input };
|
|
525
|
+
}
|
|
526
|
+
const promptError = validateRequiredPromptText(row.sdkId, dispatchRequest.input as ControlInput);
|
|
467
527
|
if (promptError) return Promise.resolve(failure(request.id, promptError.code, promptError.message));
|
|
468
528
|
if ((row.sdkId === "context.clear" || row.sdkId === "session.delete") && request.confirm !== true)
|
|
469
529
|
return Promise.resolve(
|
|
470
530
|
failure(request.id, "invalid_input", "confirm: true is required for this destructive operation."),
|
|
471
531
|
);
|
|
472
|
-
const work = () => execute(surface, row,
|
|
532
|
+
const work = () => execute(surface, row, dispatchRequest);
|
|
473
533
|
if (row.sdkId === "turn.abort_and_prompt") {
|
|
474
534
|
const cancellable = surface as PreflightCancellableSurface;
|
|
475
535
|
if (Object.hasOwn(cancellable, "cancelPendingPreflights")) cancellable.cancelPendingPreflights?.();
|
|
476
536
|
return serialize(surface, work);
|
|
477
537
|
}
|
|
478
|
-
if (row.idempotency === "idempotent" &&
|
|
538
|
+
if (row.idempotency === "idempotent" && dispatchRequest.idempotencyKey)
|
|
539
|
+
return idempotent(surface, row, dispatchRequest, work);
|
|
479
540
|
return row.idempotency === "ordered" && row.sdkId !== "retry.now" ? serialize(surface, work) : work();
|
|
480
541
|
}
|
|
@@ -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/events.ts
CHANGED
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
|
|
@@ -348,6 +354,8 @@ export class SessionSdkHost {
|
|
|
348
354
|
if (this.events.generation !== generation) return "generation_changed";
|
|
349
355
|
if (this.#readyGeneration === generation) return "already";
|
|
350
356
|
}
|
|
357
|
+
if (!this.#started || this.#stopping) return "not_prepared";
|
|
358
|
+
if (this.events.generation !== generation) return "generation_changed";
|
|
351
359
|
this.#publishReadiness();
|
|
352
360
|
return "activated";
|
|
353
361
|
}
|
|
@@ -416,6 +424,21 @@ export class SessionSdkHost {
|
|
|
416
424
|
return;
|
|
417
425
|
}
|
|
418
426
|
switch (frame.type) {
|
|
427
|
+
case "master_capability_verify": {
|
|
428
|
+
const result = this.#options.masterCapabilityVerify?.({
|
|
429
|
+
nonce: frame.nonce,
|
|
430
|
+
attestationEpoch: frame.attestationEpoch,
|
|
431
|
+
capability: frame.capability,
|
|
432
|
+
}) ?? { ok: false, nonce: "", attestationEpoch: "" };
|
|
433
|
+
await this.#send(connectionId, {
|
|
434
|
+
type: "master_capability_verify_result",
|
|
435
|
+
id: typeof frame.id === "string" ? frame.id : "",
|
|
436
|
+
ok: result.ok,
|
|
437
|
+
nonce: result.nonce,
|
|
438
|
+
attestationEpoch: result.attestationEpoch,
|
|
439
|
+
});
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
419
442
|
case "control_request": {
|
|
420
443
|
this.#observeRequest("control", connectionId, frame);
|
|
421
444
|
// Deferred readiness withholds `session_ready`, but the control
|
|
@@ -616,7 +639,11 @@ export class SessionSdkHost {
|
|
|
616
639
|
}
|
|
617
640
|
#observeRequest(kind: "control" | "query", connectionId: string, frame: SdkFrame): void {
|
|
618
641
|
try {
|
|
619
|
-
this.#options.onRequest?.(
|
|
642
|
+
this.#options.onRequest?.(
|
|
643
|
+
kind,
|
|
644
|
+
connectionId,
|
|
645
|
+
redactObservedRequestContent(redactBrokerRuntimeCapabilities(frame)),
|
|
646
|
+
);
|
|
620
647
|
} catch {
|
|
621
648
|
// Diagnostic observers must not change request handling.
|
|
622
649
|
}
|
|
@@ -18,6 +18,7 @@ export interface CursorEnvelope {
|
|
|
18
18
|
issuedAt?: number;
|
|
19
19
|
/** TTL metadata minted by `CursorRegistry.grant`: wall-clock ms when the cursor expires. */
|
|
20
20
|
expiresAt?: number;
|
|
21
|
+
purpose?: "checkpoint";
|
|
21
22
|
position: unknown;
|
|
22
23
|
direction: string;
|
|
23
24
|
pageShape: unknown;
|
|
@@ -68,6 +69,7 @@ export interface CursorQueryContext {
|
|
|
68
69
|
resourceId?: string;
|
|
69
70
|
direction: string;
|
|
70
71
|
pageShape: unknown;
|
|
72
|
+
purpose?: "checkpoint";
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
export class CursorError extends Error {
|
|
@@ -170,26 +172,68 @@ export class CursorRegistry {
|
|
|
170
172
|
cursor: string,
|
|
171
173
|
connectionId: string,
|
|
172
174
|
expected: CursorQueryContext,
|
|
173
|
-
|
|
175
|
+
_resourceKind: string,
|
|
174
176
|
resourceId: string,
|
|
175
177
|
): Promise<{ cursor: string; envelope: CursorEnvelope }> {
|
|
176
178
|
const envelope = verifyCursor(cursor, this.sessionToken);
|
|
177
|
-
if (!envelope)
|
|
178
|
-
|
|
179
|
+
if (!envelope) {
|
|
180
|
+
this.release(cursor);
|
|
181
|
+
throw new CursorError("invalid_cursor", true);
|
|
182
|
+
}
|
|
183
|
+
const source = this.#active.get(cursor);
|
|
184
|
+
if (!source || envelope.expiresAt === undefined) {
|
|
185
|
+
this.release(cursor);
|
|
186
|
+
throw new CursorError("invalid_cursor", true);
|
|
187
|
+
}
|
|
188
|
+
const validatedAt = this.now();
|
|
189
|
+
if (source.expiresAt <= validatedAt || envelope.expiresAt <= validatedAt) {
|
|
190
|
+
this.release(cursor);
|
|
191
|
+
throw new CursorError("cursor_expired");
|
|
192
|
+
}
|
|
179
193
|
if (
|
|
180
194
|
envelope.sessionId !== expected.sessionId ||
|
|
181
195
|
(expected.resource !== undefined && envelope.resource !== expected.resource) ||
|
|
182
196
|
envelope.direction !== expected.direction ||
|
|
183
|
-
canonicalJson(envelope.pageShape) !== canonicalJson(expected.pageShape)
|
|
197
|
+
canonicalJson(envelope.pageShape) !== canonicalJson(expected.pageShape) ||
|
|
198
|
+
envelope.purpose !== expected.purpose ||
|
|
199
|
+
_resourceKind !== envelope.resource ||
|
|
200
|
+
source.resourceId !== resourceId
|
|
184
201
|
)
|
|
185
202
|
throw new CursorError("invalid_input", false, "cursor does not match query");
|
|
203
|
+
// Preserve the already-validated source while removing expired target
|
|
204
|
+
// grants before capacity accounting. The shared timestamp prevents the
|
|
205
|
+
// source from changing classification at a TTL boundary mid-exchange.
|
|
206
|
+
this.sweep(cursor, validatedAt);
|
|
207
|
+
const targetConnectionCount = [...this.#active.values()].filter(
|
|
208
|
+
item => item.connectionId === connectionId,
|
|
209
|
+
).length;
|
|
210
|
+
if (
|
|
211
|
+
this.#active.size > MAX_CURSORS_PER_SESSION ||
|
|
212
|
+
targetConnectionCount - (source.connectionId === connectionId ? 1 : 0) >= MAX_CURSORS_PER_CONNECTION
|
|
213
|
+
)
|
|
214
|
+
throw new CursorError("snapshot_capacity_exceeded", false);
|
|
186
215
|
const exchanged: CursorEnvelope = {
|
|
187
216
|
...envelope,
|
|
188
217
|
nonce: randomUUID(),
|
|
189
218
|
issuedAt: undefined,
|
|
190
219
|
expiresAt: undefined,
|
|
191
220
|
};
|
|
192
|
-
const
|
|
221
|
+
const issuedAt = this.now();
|
|
222
|
+
exchanged.issuedAt = issuedAt;
|
|
223
|
+
exchanged.expiresAt = issuedAt + CURSOR_TTL_MS;
|
|
224
|
+
const exchangedCursor = signCursor(exchanged, this.sessionToken);
|
|
225
|
+
try {
|
|
226
|
+
this.revisions.transferPin(cursor, exchangedCursor);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof RevisionStoreError) throw new CursorError(error.code, false, error.message);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
this.#active.delete(cursor);
|
|
232
|
+
this.#active.set(exchangedCursor, {
|
|
233
|
+
connectionId,
|
|
234
|
+
resourceId,
|
|
235
|
+
expiresAt: exchanged.expiresAt,
|
|
236
|
+
});
|
|
193
237
|
return { cursor: exchangedCursor, envelope: exchanged };
|
|
194
238
|
}
|
|
195
239
|
|
|
@@ -227,8 +271,9 @@ export class CursorRegistry {
|
|
|
227
271
|
close(): void {
|
|
228
272
|
for (const cursor of this.#active.keys()) this.release(cursor);
|
|
229
273
|
}
|
|
230
|
-
sweep(): void {
|
|
231
|
-
for (const [cursor, active] of this.#active)
|
|
274
|
+
sweep(exceptCursor?: string, at = this.now()): void {
|
|
275
|
+
for (const [cursor, active] of this.#active)
|
|
276
|
+
if (cursor !== exceptCursor && active.expiresAt <= at) this.release(cursor);
|
|
232
277
|
this.revisions.sweep();
|
|
233
278
|
}
|
|
234
279
|
get size(): number {
|