@gajae-code/coding-agent 0.3.2 → 0.4.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.
Files changed (125) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/config/model-registry.d.ts +17 -10
  3. package/dist/types/config/models-config-schema.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +5 -0
  5. package/dist/types/edit/diff.d.ts +16 -0
  6. package/dist/types/edit/modes/replace.d.ts +7 -0
  7. package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
  8. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  9. package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
  10. package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
  11. package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
  12. package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
  13. package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
  14. package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
  15. package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
  16. package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
  17. package/dist/types/extensibility/skills.d.ts +9 -1
  18. package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
  20. package/dist/types/harness-control-plane/storage.d.ts +7 -0
  21. package/dist/types/lsp/client.d.ts +1 -0
  22. package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
  23. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
  24. package/dist/types/modes/rpc/rpc-client.d.ts +19 -1
  25. package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
  26. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
  27. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
  28. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
  29. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
  30. package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
  31. package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
  32. package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
  33. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
  34. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
  35. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
  36. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
  37. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
  38. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
  39. package/dist/types/modes/theme/theme.d.ts +2 -1
  40. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  41. package/dist/types/sdk.d.ts +7 -0
  42. package/dist/types/session/agent-session.d.ts +10 -0
  43. package/dist/types/session/blob-store.d.ts +17 -0
  44. package/dist/types/session/messages.d.ts +3 -0
  45. package/dist/types/session/session-storage.d.ts +6 -0
  46. package/dist/types/skill-state/active-state.d.ts +13 -0
  47. package/dist/types/thinking.d.ts +3 -2
  48. package/dist/types/tools/index.d.ts +3 -0
  49. package/package.json +9 -7
  50. package/src/cli.ts +14 -0
  51. package/src/commands/harness.ts +192 -7
  52. package/src/commands/ultragoal.ts +1 -21
  53. package/src/config/model-equivalence.ts +1 -1
  54. package/src/config/model-registry.ts +32 -5
  55. package/src/config/models-config-schema.ts +7 -2
  56. package/src/config/settings-schema.ts +4 -1
  57. package/src/defaults/gjc/skills/deep-interview/SKILL.md +19 -23
  58. package/src/defaults/gjc/skills/ralplan/SKILL.md +7 -7
  59. package/src/discovery/claude-plugins.ts +25 -5
  60. package/src/edit/diff.ts +64 -1
  61. package/src/edit/modes/replace.ts +60 -2
  62. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  63. package/src/extensibility/gjc-plugins/index.ts +9 -0
  64. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  65. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  66. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  67. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  68. package/src/extensibility/gjc-plugins/state.ts +29 -0
  69. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  70. package/src/extensibility/gjc-plugins/types.ts +97 -0
  71. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  72. package/src/extensibility/skills.ts +39 -7
  73. package/src/gjc-runtime/state-runtime.ts +93 -2
  74. package/src/gjc-runtime/state-writer.ts +17 -1
  75. package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
  76. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  77. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  78. package/src/harness-control-plane/storage.ts +144 -2
  79. package/src/hashline/hash.ts +23 -0
  80. package/src/hooks/skill-state.ts +2 -0
  81. package/src/internal-urls/docs-index.generated.ts +5 -5
  82. package/src/lsp/client.ts +7 -0
  83. package/src/modes/acp/acp-agent.ts +25 -2
  84. package/src/modes/bridge/bridge-mode.ts +124 -2
  85. package/src/modes/controllers/input-controller.ts +14 -2
  86. package/src/modes/prompt-action-autocomplete.ts +49 -10
  87. package/src/modes/rpc/rpc-client.ts +79 -3
  88. package/src/modes/rpc/rpc-mode.ts +67 -0
  89. package/src/modes/rpc/rpc-types.ts +224 -2
  90. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  91. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  92. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  93. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  94. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  95. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  96. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  97. package/src/modes/shared/agent-wire/responses.ts +2 -2
  98. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  99. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  100. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  101. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  102. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  103. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  104. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  105. package/src/modes/theme/theme.ts +6 -0
  106. package/src/prompts/system/system-prompt.md +9 -0
  107. package/src/runtime-mcp/client.ts +7 -4
  108. package/src/runtime-mcp/manager.ts +45 -13
  109. package/src/runtime-mcp/transports/http.ts +40 -14
  110. package/src/runtime-mcp/transports/stdio.ts +11 -10
  111. package/src/sdk.ts +47 -0
  112. package/src/session/agent-session.ts +211 -2
  113. package/src/session/blob-store.ts +84 -0
  114. package/src/session/messages.ts +3 -0
  115. package/src/session/session-manager.ts +390 -33
  116. package/src/session/session-storage.ts +26 -0
  117. package/src/setup/provider-onboarding.ts +2 -2
  118. package/src/skill-state/active-state.ts +89 -1
  119. package/src/task/discovery.ts +7 -1
  120. package/src/task/executor.ts +16 -2
  121. package/src/thinking.ts +8 -2
  122. package/src/tools/ask.ts +39 -9
  123. package/src/tools/index.ts +3 -0
  124. package/src/tools/skill.ts +15 -3
  125. package/src/utils/edit-mode.ts +1 -1
@@ -72,7 +72,13 @@ export type RpcCommand =
72
72
 
73
73
  // Login
74
74
  | { id?: string; type: "get_login_providers" }
75
- | { id?: string; type: "login"; providerId: string };
75
+ | { id?: string; type: "login"; providerId: string }
76
+
77
+ // Unattended control plane (#318/#319 declaration handled here at #315 contract level)
78
+ | { id?: string; type: "negotiate_unattended"; declaration: RpcUnattendedDeclaration }
79
+
80
+ // Workflow gate answer (inbound response to a workflow_gate event)
81
+ | ({ id?: string; type: "workflow_gate_response" } & RpcWorkflowGateResponse);
76
82
 
77
83
  // ============================================================================
78
84
  // RPC State
@@ -209,8 +215,18 @@ export type RpcResponse =
209
215
  }
210
216
  | { id?: string; type: "response"; command: "login"; success: true; data: { providerId: string } }
211
217
 
218
+ // Unattended + workflow gates
219
+ | { id?: string; type: "response"; command: "negotiate_unattended"; success: true; data: RpcUnattendedAccepted }
220
+ | {
221
+ id?: string;
222
+ type: "response";
223
+ command: "workflow_gate_response";
224
+ success: true;
225
+ data: RpcWorkflowGateResolution;
226
+ }
227
+
212
228
  // Error response (any command can fail)
213
- | { id?: string; type: "response"; command: string; success: false; error: string };
229
+ | { id?: string; type: "response"; command: string; success: false; error: string | object };
214
230
 
215
231
  // ============================================================================
216
232
  // Extension UI Events (stdout)
@@ -376,3 +392,209 @@ export type RpcExtensionUIResponse =
376
392
  // ============================================================================
377
393
 
378
394
  export type RpcCommandType = RpcCommand["type"];
395
+
396
+ // ============================================================================
397
+ // Workflow Gate Contract (#315)
398
+ // ============================================================================
399
+
400
+ /**
401
+ * Lifecycle stages that emit machine-addressable gates. v1 is single-agent;
402
+ * `team` parallel execution over RPC is deferred, so it is intentionally absent
403
+ * from this union. Gate construction rejects any other stage value.
404
+ */
405
+ export type RpcWorkflowStage = "deep-interview" | "ralplan" | "ultragoal";
406
+
407
+ /** Reserved stage names that are explicitly not part of the v1 contract. */
408
+ export const RESERVED_WORKFLOW_STAGES: readonly string[] = ["team"];
409
+
410
+ export type RpcWorkflowGateKind = "question" | "approval" | "execution";
411
+
412
+ /**
413
+ * The documented JSON Schema 2020-12 subset supported by the gate validator.
414
+ * Schemas containing any keyword outside this shape are rejected at gate
415
+ * construction time so the server never advertises a schema it cannot validate.
416
+ */
417
+ export interface RpcJsonSchema {
418
+ type?: "string" | "number" | "integer" | "boolean" | "object" | "array" | "null";
419
+ enum?: unknown[];
420
+ const?: unknown;
421
+ properties?: Record<string, RpcJsonSchema>;
422
+ required?: string[];
423
+ additionalProperties?: boolean | RpcJsonSchema;
424
+ items?: RpcJsonSchema;
425
+ minLength?: number;
426
+ maxLength?: number;
427
+ minItems?: number;
428
+ maxItems?: number;
429
+ uniqueItems?: boolean;
430
+ minimum?: number;
431
+ maximum?: number;
432
+ title?: string;
433
+ description?: string;
434
+ oneOf?: RpcJsonSchema[];
435
+ anyOf?: RpcJsonSchema[];
436
+ }
437
+
438
+ export interface RpcWorkflowGateOption {
439
+ value: unknown;
440
+ label: string;
441
+ description?: string;
442
+ }
443
+
444
+ export interface RpcWorkflowGateContext {
445
+ title?: string;
446
+ prompt?: string;
447
+ summary?: string;
448
+ stage_state?: Record<string, unknown>;
449
+ artifact_refs?: Array<{ kind: string; path?: string; sha256?: string }>;
450
+ language?: string;
451
+ }
452
+
453
+ /** Outbound event: a machine-addressable workflow gate awaiting an answer. */
454
+ export interface RpcWorkflowGate {
455
+ type: "workflow_gate";
456
+ /** Run-scoped, monotonic, stable id (e.g. `wg_<run>_<stage>_000001`). */
457
+ gate_id: string;
458
+ stage: RpcWorkflowStage;
459
+ kind: RpcWorkflowGateKind;
460
+ schema: RpcJsonSchema;
461
+ /** Canonical hash of `schema`; advertised hash must equal server validation hash. */
462
+ schema_hash: string;
463
+ options?: RpcWorkflowGateOption[];
464
+ context: RpcWorkflowGateContext;
465
+ created_at: string;
466
+ required: true;
467
+ }
468
+
469
+ /** Inbound: the agent's answer to a workflow gate. */
470
+ export interface RpcWorkflowGateResponse {
471
+ gate_id: string;
472
+ answer: unknown;
473
+ /** Optional idempotency key; same key + body returns the cached resolution. */
474
+ idempotency_key?: string;
475
+ }
476
+
477
+ /** Outcome of resolving a gate, surfaced back to the answering client. */
478
+ export interface RpcWorkflowGateResolution {
479
+ gate_id: string;
480
+ status: "accepted" | "rejected";
481
+ answer_hash: string;
482
+ resolved_at: string;
483
+ /** Present only when `status === "rejected"`. */
484
+ error?: RpcWorkflowGateValidationError;
485
+ }
486
+
487
+ /** Typed error shape for schema validation failures (#315 acceptance). */
488
+ export interface RpcWorkflowGateValidationError {
489
+ code: "invalid_workflow_gate_answer";
490
+ gate_id: string;
491
+ schema_hash: string;
492
+ errors: Array<{ path: string; keyword: string; message: string; expected?: unknown }>;
493
+ }
494
+
495
+ // ============================================================================
496
+ // Unattended Declaration Contract (#318/#319 — declared at #315 boundary)
497
+ // ============================================================================
498
+
499
+ export interface RpcUnattendedBudget {
500
+ max_tokens: number;
501
+ max_tool_calls: number;
502
+ max_wall_time_ms: number;
503
+ max_cost_usd: number;
504
+ }
505
+
506
+ export interface RpcUnattendedDeclaration {
507
+ /** Identity of the operating external agent, recorded in the audit trail. */
508
+ actor: string;
509
+ budget: RpcUnattendedBudget;
510
+ /** Coarse command scopes the agent may use (maps to BridgeCommandScope). */
511
+ scopes: string[];
512
+ /** Action classes the agent is allowed to perform (default-deny otherwise). */
513
+ action_allowlist: string[];
514
+ }
515
+
516
+ export interface RpcUnattendedAccepted {
517
+ run_id: string;
518
+ actor: string;
519
+ budget: RpcUnattendedBudget;
520
+ scopes: string[];
521
+ action_allowlist: string[];
522
+ accepted_at: string;
523
+ }
524
+
525
+ export type RpcBudgetMetric = "tokens" | "tool_calls" | "wall_time" | "cost";
526
+
527
+ /** Typed payload emitted when a declared budget cap is breached (#318). */
528
+ export interface RpcBudgetExceeded {
529
+ code: "budget_exceeded";
530
+ metric: RpcBudgetMetric;
531
+ limit: number;
532
+ observed: number;
533
+ /** The accounting phase that detected the breach. */
534
+ phase: string;
535
+ run_id: string;
536
+ session_id?: string;
537
+ /** `aborting` = breach detected, async abort initiated; settled status follows in audit. */
538
+ abort_status: "aborting" | "aborted" | "abort_failed";
539
+ }
540
+
541
+ export type RpcUnattendedRefusalCode =
542
+ | "unattended_not_negotiated"
543
+ | "incomplete_budget"
544
+ | "unsupported_budget_metric"
545
+ | "invalid_unattended_declaration"
546
+ | "unattended_aborted";
547
+
548
+ /** Typed refusal emitted when unattended mode cannot start or continue (fail-closed). */
549
+ export interface RpcUnattendedRefused {
550
+ code: RpcUnattendedRefusalCode;
551
+ message: string;
552
+ }
553
+
554
+ // ============================================================================
555
+ // Unattended Action Authorization Contract (#319)
556
+ // ============================================================================
557
+
558
+ /** v1 action taxonomy: every authorized operation maps to one of these classes. */
559
+ export type RpcUnattendedActionClass =
560
+ | "command.prompt"
561
+ | "command.control"
562
+ | "command.bash"
563
+ | "command.export"
564
+ | "command.session"
565
+ | "command.model"
566
+ | "command.message_read"
567
+ | "command.host_tools"
568
+ | "command.host_uri"
569
+ | "command.admin"
570
+ | "bash.readonly"
571
+ | "bash.mutating"
572
+ | "bash.destructive"
573
+ | "git.force_push"
574
+ | "file.delete"
575
+ | "file.write"
576
+ | "host_tool.invoke"
577
+ | "host_uri.read"
578
+ | "host_uri.write"
579
+ | "auth.login";
580
+
581
+ /** Typed error when a command's coarse scope is not in the declared allowlist. */
582
+ export interface RpcScopeDenied {
583
+ code: "scope_denied";
584
+ scope: string;
585
+ command?: string;
586
+ run_id: string;
587
+ session_id?: string;
588
+ /** Always true: enforcement happens before the side effect runs. */
589
+ pre_side_effect: true;
590
+ }
591
+
592
+ /** Typed error when an action class is not in the declared allowlist (default-deny). */
593
+ export interface RpcActionDenied {
594
+ code: "action_denied";
595
+ action: string;
596
+ command?: string;
597
+ run_id: string;
598
+ session_id?: string;
599
+ pre_side_effect: true;
600
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Ralplan approval + ultragoal execution gate mapping (#317).
3
+ *
4
+ * Maps the two human-gated lifecycle decisions onto `workflow_gate` events:
5
+ * - ralplan `pending approval` -> `workflow_gate` { kind: "approval" } whose
6
+ * answer is one of approve / request-changes / reject (+ optional comments);
7
+ * - ultragoal execution sign-off -> `workflow_gate` { kind: "execution" } whose
8
+ * answer is approve / decline (+ optional reason).
9
+ *
10
+ * Gates remain mandatory; the external agent substitutes for the human at the
11
+ * answer boundary only. Declining / requesting changes is honored and is NEVER
12
+ * silently treated as approval.
13
+ *
14
+ * This is the pure mapping primitive; routing ralplan/ultragoal through it when
15
+ * an unattended controller + gate broker are attached is wired with the transport
16
+ * in #321 and exercised end-to-end by #323.
17
+ */
18
+ import type { RpcJsonSchema, RpcWorkflowGateContext } from "../../rpc/rpc-types";
19
+ import type { OpenGateInput } from "./workflow-gate-broker";
20
+
21
+ export type ApprovalDecision = "approve" | "request-changes" | "reject";
22
+ export type ExecutionDecision = "approve" | "decline";
23
+
24
+ export interface ApprovalGateAnswer {
25
+ decision: ApprovalDecision;
26
+ comments?: string;
27
+ }
28
+
29
+ export interface ExecutionGateAnswer {
30
+ decision: ExecutionDecision;
31
+ reason?: string;
32
+ }
33
+
34
+ export interface ApprovalGateResult {
35
+ approved: boolean;
36
+ decision: ApprovalDecision;
37
+ comments?: string;
38
+ }
39
+
40
+ export interface ExecutionGateResult {
41
+ approved: boolean;
42
+ decision: ExecutionDecision;
43
+ reason?: string;
44
+ }
45
+
46
+ export class ApprovalGateError extends Error {
47
+ constructor(
48
+ readonly code: "invalid_answer_shape" | "unknown_decision" | "missing_comments",
49
+ message: string,
50
+ ) {
51
+ super(message);
52
+ this.name = "ApprovalGateError";
53
+ }
54
+ }
55
+
56
+ const APPROVAL_DECISIONS: ApprovalDecision[] = ["approve", "request-changes", "reject"];
57
+ const EXECUTION_DECISIONS: ExecutionDecision[] = ["approve", "decline"];
58
+
59
+ /** Build the ralplan `pending approval` -> `workflow_gate { kind: "approval" }` open-input. */
60
+ export function approvalGate(context: RpcWorkflowGateContext = {}): OpenGateInput {
61
+ const schema: RpcJsonSchema = {
62
+ type: "object",
63
+ properties: {
64
+ decision: { type: "string", enum: APPROVAL_DECISIONS },
65
+ comments: { type: "string", description: "required when requesting changes" },
66
+ },
67
+ required: ["decision"],
68
+ additionalProperties: false,
69
+ };
70
+ return {
71
+ stage: "ralplan",
72
+ kind: "approval",
73
+ schema,
74
+ options: APPROVAL_DECISIONS.map(d => ({ value: d, label: d })),
75
+ context: { title: context.title ?? "Approve the plan?", ...context },
76
+ };
77
+ }
78
+
79
+ /** Build the ultragoal execution sign-off -> `workflow_gate { kind: "execution" }` open-input. */
80
+ export function executionGate(context: RpcWorkflowGateContext = {}): OpenGateInput {
81
+ const schema: RpcJsonSchema = {
82
+ type: "object",
83
+ properties: {
84
+ decision: { type: "string", enum: EXECUTION_DECISIONS },
85
+ reason: { type: "string", description: "optional rationale; required when declining" },
86
+ },
87
+ required: ["decision"],
88
+ additionalProperties: false,
89
+ };
90
+ return {
91
+ stage: "ultragoal",
92
+ kind: "execution",
93
+ schema,
94
+ options: EXECUTION_DECISIONS.map(d => ({ value: d, label: d })),
95
+ context: { title: context.title ?? "Approve execution?", ...context },
96
+ };
97
+ }
98
+
99
+ function decisionField(answer: unknown): string {
100
+ if (typeof answer !== "object" || answer === null) {
101
+ throw new ApprovalGateError("invalid_answer_shape", "answer must be an object with a `decision` field");
102
+ }
103
+ const decision = (answer as { decision?: unknown }).decision;
104
+ if (typeof decision !== "string") {
105
+ throw new ApprovalGateError("invalid_answer_shape", "answer.decision must be a string");
106
+ }
107
+ return decision;
108
+ }
109
+
110
+ /**
111
+ * Decode a ralplan approval answer. `request-changes` requires comments and is
112
+ * NEVER treated as approval; only an explicit `approve` advances.
113
+ */
114
+ export function decodeApproval(answer: unknown): ApprovalGateResult {
115
+ const decision = decisionField(answer);
116
+ if (!APPROVAL_DECISIONS.includes(decision as ApprovalDecision)) {
117
+ throw new ApprovalGateError("unknown_decision", `unknown approval decision: ${decision}`);
118
+ }
119
+ const comments = (answer as { comments?: unknown }).comments;
120
+ if (comments !== undefined && typeof comments !== "string") {
121
+ throw new ApprovalGateError("invalid_answer_shape", "answer.comments must be a string");
122
+ }
123
+ if (decision === "request-changes" && (comments === undefined || comments.trim() === "")) {
124
+ throw new ApprovalGateError("missing_comments", "comments are required when requesting changes");
125
+ }
126
+ return {
127
+ approved: decision === "approve",
128
+ decision: decision as ApprovalDecision,
129
+ comments: comments as string | undefined,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Decode an ultragoal execution answer. Only an explicit `approve` advances;
135
+ * `decline` is honored and never silently approved.
136
+ */
137
+ export function decodeExecution(answer: unknown): ExecutionGateResult {
138
+ const decision = decisionField(answer);
139
+ if (!EXECUTION_DECISIONS.includes(decision as ExecutionDecision)) {
140
+ throw new ApprovalGateError("unknown_decision", `unknown execution decision: ${decision}`);
141
+ }
142
+ const reason = (answer as { reason?: unknown }).reason;
143
+ if (reason !== undefined && typeof reason !== "string") {
144
+ throw new ApprovalGateError("invalid_answer_shape", "answer.reason must be a string");
145
+ }
146
+ return {
147
+ approved: decision === "approve",
148
+ decision: decision as ExecutionDecision,
149
+ reason: reason as string | undefined,
150
+ };
151
+ }
@@ -10,8 +10,19 @@ import type {
10
10
  RpcHostUriSchemeDefinition,
11
11
  RpcResponse,
12
12
  RpcSessionState,
13
+ RpcUnattendedAccepted,
14
+ RpcUnattendedDeclaration,
15
+ RpcWorkflowGateResolution,
16
+ RpcWorkflowGateResponse,
13
17
  } from "../../rpc/rpc-types";
14
18
  import { rpcError, rpcSuccess } from "./responses";
19
+ import {
20
+ ActionDeniedError,
21
+ ScopeDeniedError,
22
+ UnattendedBudgetExceededError,
23
+ UnattendedNegotiationError,
24
+ } from "./unattended-run-controller";
25
+ import { WorkflowGateBrokerError } from "./workflow-gate-broker";
15
26
 
16
27
  export type RpcCommandDispatchOutput = (obj: RpcResponse | RpcExtensionUIRequest | object) => void;
17
28
 
@@ -23,12 +34,28 @@ export interface RpcHostUriRegistry {
23
34
  setSchemes(schemes: RpcHostUriSchemeDefinition[]): string[];
24
35
  }
25
36
 
37
+ /**
38
+ * Optional unattended control plane wired into RPC dispatch (#318/#319/#323).
39
+ * When present, `negotiate_unattended` and `workflow_gate_response` route here
40
+ * instead of falling through to the unknown-command path.
41
+ */
42
+ export interface RpcUnattendedControlPlane {
43
+ /** Enter unattended mode (fail-closed); throws an Error on refusal. */
44
+ negotiate(declaration: RpcUnattendedDeclaration): RpcUnattendedAccepted;
45
+ /** Resolve a pending workflow gate with the agent's answer. */
46
+ resolveGate(response: RpcWorkflowGateResponse): Promise<RpcWorkflowGateResolution>;
47
+ isUnattended?(): boolean;
48
+ preflightCommand?(command: RpcCommand): void;
49
+ reconcileUsage?(phase?: string): void;
50
+ }
51
+
26
52
  export interface RpcCommandDispatchContext {
27
53
  session: AgentSession;
28
54
  output: RpcCommandDispatchOutput;
29
55
  hostToolRegistry: RpcHostToolRegistry;
30
56
  hostUriRegistry: RpcHostUriRegistry;
31
57
  createUiContext: () => Pick<ExtensionUIContext, "notify">;
58
+ unattendedControlPlane?: RpcUnattendedControlPlane;
32
59
  }
33
60
 
34
61
  export function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): RpcHostToolDefinition[] {
@@ -55,12 +82,50 @@ export function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): Rp
55
82
  });
56
83
  }
57
84
 
85
+ function serializeRpcDispatchError(err: unknown): string | object {
86
+ if (
87
+ err instanceof ScopeDeniedError ||
88
+ err instanceof ActionDeniedError ||
89
+ err instanceof UnattendedBudgetExceededError
90
+ ) {
91
+ return err.payload;
92
+ }
93
+ if (err instanceof UnattendedNegotiationError) {
94
+ return { code: err.code, message: err.message };
95
+ }
96
+ if (err instanceof WorkflowGateBrokerError) {
97
+ return { code: err.code, message: err.message };
98
+ }
99
+ return err instanceof Error ? err.message : String(err);
100
+ }
101
+
58
102
  export async function dispatchRpcCommand(
59
103
  command: RpcCommand,
60
104
  context: RpcCommandDispatchContext,
61
105
  ): Promise<RpcResponse> {
62
- const { session, output, hostToolRegistry, hostUriRegistry, createUiContext } = context;
106
+ const { session, output, hostToolRegistry, hostUriRegistry, createUiContext, unattendedControlPlane } = context;
63
107
  const id = command.id;
108
+ const typedError = (cmd: string, err: unknown): RpcResponse => rpcError(id, cmd, serializeRpcDispatchError(err));
109
+ const preflight = (): RpcResponse | undefined => {
110
+ if (!unattendedControlPlane?.isUnattended?.() || command.type === "negotiate_unattended") return undefined;
111
+ try {
112
+ unattendedControlPlane.preflightCommand?.(command);
113
+ return undefined;
114
+ } catch (err) {
115
+ return typedError(command.type, err);
116
+ }
117
+ };
118
+ const reconcile = (phase = `${command.type} post-dispatch`): RpcResponse | undefined => {
119
+ if (!unattendedControlPlane?.isUnattended?.()) return undefined;
120
+ try {
121
+ unattendedControlPlane.reconcileUsage?.(phase);
122
+ return undefined;
123
+ } catch (err) {
124
+ return typedError(command.type, err);
125
+ }
126
+ };
127
+ const denied = preflight();
128
+ if (denied) return denied;
64
129
 
65
130
  switch (command.type) {
66
131
  case "prompt": {
@@ -69,8 +134,8 @@ export async function dispatchRpcCommand(
69
134
  images: command.images,
70
135
  streamingBehavior: command.streamingBehavior,
71
136
  })
72
- .catch(e => output(rpcError(id, "prompt", e.message)));
73
- return rpcSuccess(id, "prompt");
137
+ .catch(e => output(rpcError(id, "prompt", serializeRpcDispatchError(e))));
138
+ return reconcile() ?? rpcSuccess(id, "prompt");
74
139
  }
75
140
 
76
141
  case "steer": {
@@ -223,7 +288,7 @@ export async function dispatchRpcCommand(
223
288
 
224
289
  case "bash": {
225
290
  const result = await session.executeBash(command.command);
226
- return rpcSuccess(id, "bash", result);
291
+ return reconcile() ?? rpcSuccess(id, "bash", result);
227
292
  }
228
293
 
229
294
  case "abort_bash": {
@@ -333,6 +398,34 @@ export async function dispatchRpcCommand(
333
398
  }
334
399
  }
335
400
 
401
+ case "negotiate_unattended": {
402
+ if (!unattendedControlPlane) {
403
+ return rpcError(id, "negotiate_unattended", "unattended mode is not available on this session");
404
+ }
405
+ try {
406
+ const accepted = unattendedControlPlane.negotiate(command.declaration);
407
+ return rpcSuccess(id, "negotiate_unattended", accepted);
408
+ } catch (err) {
409
+ return typedError("negotiate_unattended", err);
410
+ }
411
+ }
412
+
413
+ case "workflow_gate_response": {
414
+ if (!unattendedControlPlane) {
415
+ return rpcError(id, "workflow_gate_response", "workflow gates are not available on this session");
416
+ }
417
+ try {
418
+ const resolution = await unattendedControlPlane.resolveGate({
419
+ gate_id: command.gate_id,
420
+ answer: command.answer,
421
+ idempotency_key: command.idempotency_key,
422
+ });
423
+ return rpcSuccess(id, "workflow_gate_response", resolution);
424
+ } catch (err) {
425
+ return typedError("workflow_gate_response", err);
426
+ }
427
+ }
428
+
336
429
  default: {
337
430
  const unknownCommand = command as { type: string };
338
431
  return rpcError(undefined, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);
@@ -17,7 +17,7 @@ function stringField(value: Record<string, unknown>, key: string): boolean {
17
17
  return typeof value[key] === "string";
18
18
  }
19
19
 
20
- const THINKING_LEVELS = new Set(["inherit", "off", "minimal", "low", "medium", "high", "xhigh"]);
20
+ const THINKING_LEVELS = new Set(["inherit", "off", "minimal", "low", "medium", "high", "xhigh", "max"]);
21
21
  const TODO_STATUSES = new Set(["pending", "in_progress", "completed", "abandoned"]);
22
22
 
23
23
  function optionalBoolean(value: unknown): boolean {
@@ -61,6 +61,26 @@ function hostUriScheme(value: unknown): boolean {
61
61
  );
62
62
  }
63
63
 
64
+ function unattendedBudget(value: unknown): boolean {
65
+ return (
66
+ isRecord(value) &&
67
+ typeof value.max_tokens === "number" &&
68
+ typeof value.max_tool_calls === "number" &&
69
+ typeof value.max_wall_time_ms === "number" &&
70
+ typeof value.max_cost_usd === "number"
71
+ );
72
+ }
73
+
74
+ function unattendedDeclaration(value: unknown): boolean {
75
+ return (
76
+ isRecord(value) &&
77
+ typeof value.actor === "string" &&
78
+ unattendedBudget(value.budget) &&
79
+ stringArray(value.scopes) &&
80
+ stringArray(value.action_allowlist)
81
+ );
82
+ }
83
+
64
84
  export function isRpcCommand(value: unknown): value is RpcCommand {
65
85
  if (!isRecord(value) || !optionalString(value.id) || !isRpcCommandType(value.type)) return false;
66
86
  switch (value.type) {
@@ -127,5 +147,9 @@ export function isRpcCommand(value: unknown): value is RpcCommand {
127
147
  return optionalString(value.customInstructions);
128
148
  case "login":
129
149
  return stringField(value, "providerId");
150
+ case "negotiate_unattended":
151
+ return unattendedDeclaration(value.declaration);
152
+ case "workflow_gate_response":
153
+ return stringField(value, "gate_id") && "answer" in value && optionalString(value.idempotency_key);
130
154
  }
131
155
  }