@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.
- package/CHANGELOG.md +39 -0
- package/dist/types/config/model-registry.d.ts +17 -10
- package/dist/types/config/models-config-schema.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +5 -0
- package/dist/types/edit/diff.d.ts +16 -0
- package/dist/types/edit/modes/replace.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
- package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
- package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
- package/dist/types/extensibility/skills.d.ts +9 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
- package/dist/types/harness-control-plane/storage.d.ts +7 -0
- package/dist/types/lsp/client.d.ts +1 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +19 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/blob-store.d.ts +17 -0
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/skill-state/active-state.d.ts +13 -0
- package/dist/types/thinking.d.ts +3 -2
- package/dist/types/tools/index.d.ts +3 -0
- package/package.json +9 -7
- package/src/cli.ts +14 -0
- package/src/commands/harness.ts +192 -7
- package/src/commands/ultragoal.ts +1 -21
- package/src/config/model-equivalence.ts +1 -1
- package/src/config/model-registry.ts +32 -5
- package/src/config/models-config-schema.ts +7 -2
- package/src/config/settings-schema.ts +4 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +19 -23
- package/src/defaults/gjc/skills/ralplan/SKILL.md +7 -7
- package/src/discovery/claude-plugins.ts +25 -5
- package/src/edit/diff.ts +64 -1
- package/src/edit/modes/replace.ts +60 -2
- package/src/extensibility/gjc-plugins/activation.ts +87 -0
- package/src/extensibility/gjc-plugins/index.ts +9 -0
- package/src/extensibility/gjc-plugins/injection.ts +114 -0
- package/src/extensibility/gjc-plugins/loader.ts +131 -0
- package/src/extensibility/gjc-plugins/paths.ts +66 -0
- package/src/extensibility/gjc-plugins/schema.ts +79 -0
- package/src/extensibility/gjc-plugins/state.ts +29 -0
- package/src/extensibility/gjc-plugins/tools.ts +47 -0
- package/src/extensibility/gjc-plugins/types.ts +97 -0
- package/src/extensibility/gjc-plugins/validation.ts +76 -0
- package/src/extensibility/skills.ts +39 -7
- package/src/gjc-runtime/state-runtime.ts +93 -2
- package/src/gjc-runtime/state-writer.ts +17 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
- package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
- package/src/gjc-runtime/workflow-manifest.ts +2 -2
- package/src/harness-control-plane/storage.ts +144 -2
- package/src/hashline/hash.ts +23 -0
- package/src/hooks/skill-state.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +7 -0
- package/src/modes/acp/acp-agent.ts +25 -2
- package/src/modes/bridge/bridge-mode.ts +124 -2
- package/src/modes/controllers/input-controller.ts +14 -2
- package/src/modes/prompt-action-autocomplete.ts +49 -10
- package/src/modes/rpc/rpc-client.ts +79 -3
- package/src/modes/rpc/rpc-mode.ts +67 -0
- package/src/modes/rpc/rpc-types.ts +224 -2
- package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
- package/src/modes/shared/agent-wire/command-validation.ts +25 -1
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
- package/src/modes/shared/agent-wire/handshake.ts +43 -3
- package/src/modes/shared/agent-wire/protocol.ts +7 -0
- package/src/modes/shared/agent-wire/responses.ts +2 -2
- package/src/modes/shared/agent-wire/scopes.ts +2 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
- package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/system/system-prompt.md +9 -0
- package/src/runtime-mcp/client.ts +7 -4
- package/src/runtime-mcp/manager.ts +45 -13
- package/src/runtime-mcp/transports/http.ts +40 -14
- package/src/runtime-mcp/transports/stdio.ts +11 -10
- package/src/sdk.ts +47 -0
- package/src/session/agent-session.ts +211 -2
- package/src/session/blob-store.ts +84 -0
- package/src/session/messages.ts +3 -0
- package/src/session/session-manager.ts +390 -33
- package/src/session/session-storage.ts +26 -0
- package/src/setup/provider-onboarding.ts +2 -2
- package/src/skill-state/active-state.ts +89 -1
- package/src/task/discovery.ts +7 -1
- package/src/task/executor.ts +16 -2
- package/src/thinking.ts +8 -2
- package/src/tools/ask.ts +39 -9
- package/src/tools/index.ts +3 -0
- package/src/tools/skill.ts +15 -3
- 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
|
|
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
|
}
|