@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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-interview gate mapping (#316).
|
|
3
|
+
*
|
|
4
|
+
* Converts deep-interview `ask`-tool questions into machine-addressable
|
|
5
|
+
* `workflow_gate` { kind: "question" } events (option set + free-text shape
|
|
6
|
+
* encoded in `schema`/`options`) and decodes a `workflow_gate_response` answer
|
|
7
|
+
* back into the exact QuestionResult shape the human path produces, so ambiguity
|
|
8
|
+
* scoring/state updates proceed identically whether a human or an agent answers.
|
|
9
|
+
*
|
|
10
|
+
* This is the pure mapping primitive. Routing the ask tool through it (instead of
|
|
11
|
+
* the interactive select/editor UI) when an unattended controller + gate broker
|
|
12
|
+
* are attached is wired with the transport in #321 and exercised by #323.
|
|
13
|
+
*/
|
|
14
|
+
import type { RpcJsonSchema } from "../../rpc/rpc-types";
|
|
15
|
+
import type { OpenGateInput } from "./workflow-gate-broker";
|
|
16
|
+
|
|
17
|
+
/** "Other (type your own)" sentinel, mirroring the interactive ask tool. */
|
|
18
|
+
export const GATE_OTHER_OPTION = "Other (type your own)";
|
|
19
|
+
|
|
20
|
+
export interface AskGateQuestion {
|
|
21
|
+
id: string;
|
|
22
|
+
question: string;
|
|
23
|
+
options: Array<{ label: string }>;
|
|
24
|
+
multi?: boolean;
|
|
25
|
+
recommended?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AskGateResult {
|
|
29
|
+
id: string;
|
|
30
|
+
question: string;
|
|
31
|
+
options: string[];
|
|
32
|
+
multi: boolean;
|
|
33
|
+
selectedOptions: string[];
|
|
34
|
+
customInput?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The answer shape an agent returns for a deep-interview question gate.
|
|
39
|
+
*
|
|
40
|
+
* `selected` are picked option labels; free text is conveyed by `other: true`
|
|
41
|
+
* plus `custom`, encoded separately from `selected` so a real option whose label
|
|
42
|
+
* happens to equal the display sentinel can never collide with the free-text path.
|
|
43
|
+
*/
|
|
44
|
+
export interface DeepInterviewGateAnswer {
|
|
45
|
+
selected: string[];
|
|
46
|
+
other?: boolean;
|
|
47
|
+
custom?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class DeepInterviewGateError extends Error {
|
|
51
|
+
constructor(
|
|
52
|
+
readonly code:
|
|
53
|
+
| "invalid_answer_shape"
|
|
54
|
+
| "unknown_option"
|
|
55
|
+
| "multi_not_allowed"
|
|
56
|
+
| "missing_custom"
|
|
57
|
+
| "empty_selection"
|
|
58
|
+
| "duplicate_selection",
|
|
59
|
+
message: string,
|
|
60
|
+
) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = "DeepInterviewGateError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function deepInterviewQuestionState(questionText: string): Record<string, unknown> {
|
|
67
|
+
const roundMatch = /^Round\s+(\d+)\s+\|\s+([^|]+?)\s+\|\s+Ambiguity:\s*(.+?)\s*$/im.exec(questionText);
|
|
68
|
+
const state: Record<string, unknown> = {};
|
|
69
|
+
if (roundMatch) {
|
|
70
|
+
const round = Number(roundMatch[1]);
|
|
71
|
+
if (Number.isFinite(round)) state.round = round;
|
|
72
|
+
const mode = roundMatch[2]?.trim();
|
|
73
|
+
if (mode) {
|
|
74
|
+
state.mode = mode;
|
|
75
|
+
const normalized = mode.toLowerCase();
|
|
76
|
+
if (normalized.includes("topology")) state.topology_gate = true;
|
|
77
|
+
if (/(contrarian|simplifier|ontologist)/u.test(normalized)) state.challenge_mode = normalized;
|
|
78
|
+
}
|
|
79
|
+
const ambiguity = roundMatch[3]?.trim();
|
|
80
|
+
if (ambiguity) state.ambiguity = ambiguity;
|
|
81
|
+
}
|
|
82
|
+
if (/Round\s+0\s+\|\s+Topology confirmation/im.test(questionText)) {
|
|
83
|
+
state.round = 0;
|
|
84
|
+
state.mode = "Topology confirmation";
|
|
85
|
+
state.topology_gate = true;
|
|
86
|
+
}
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJsonSchema {
|
|
91
|
+
const multi = question.multi ?? false;
|
|
92
|
+
const selectedItems: RpcJsonSchema = { type: "string", enum: labels };
|
|
93
|
+
const selectedBase: RpcJsonSchema = { type: "array", items: selectedItems, uniqueItems: true };
|
|
94
|
+
const selectedOnly: RpcJsonSchema = {
|
|
95
|
+
...selectedBase,
|
|
96
|
+
minItems: 1,
|
|
97
|
+
...(multi ? {} : { maxItems: 1 }),
|
|
98
|
+
};
|
|
99
|
+
const selectedWithOther: RpcJsonSchema = {
|
|
100
|
+
...selectedBase,
|
|
101
|
+
...(multi ? {} : { maxItems: 0 }),
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
selected: selectedBase,
|
|
107
|
+
other: { type: "boolean", description: "set true to provide a free-text answer in `custom`" },
|
|
108
|
+
custom: { type: "string", minLength: 1, description: "free-text answer; required when `other` is true" },
|
|
109
|
+
},
|
|
110
|
+
required: ["selected"],
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
anyOf: [
|
|
113
|
+
{
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: { selected: selectedOnly, other: { const: false } },
|
|
116
|
+
required: ["selected"],
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
selected: selectedWithOther,
|
|
123
|
+
other: { const: true },
|
|
124
|
+
custom: { type: "string", minLength: 1 },
|
|
125
|
+
},
|
|
126
|
+
required: ["selected", "other", "custom"],
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Build the `workflow_gate` open-input for one deep-interview question. */
|
|
134
|
+
export function questionToGate(question: AskGateQuestion): OpenGateInput {
|
|
135
|
+
const labels = question.options.map(o => o.label);
|
|
136
|
+
const schema = questionAnswerSchema(question, labels);
|
|
137
|
+
return {
|
|
138
|
+
stage: "deep-interview",
|
|
139
|
+
kind: "question",
|
|
140
|
+
schema,
|
|
141
|
+
options: question.options.map((o, i) => ({
|
|
142
|
+
value: o.label,
|
|
143
|
+
label: o.label,
|
|
144
|
+
description: i === question.recommended ? "recommended" : undefined,
|
|
145
|
+
})),
|
|
146
|
+
context: {
|
|
147
|
+
title: question.question,
|
|
148
|
+
prompt: question.question,
|
|
149
|
+
stage_state: {
|
|
150
|
+
question_id: question.id,
|
|
151
|
+
multi: question.multi ?? false,
|
|
152
|
+
options: labels,
|
|
153
|
+
other_option: GATE_OTHER_OPTION,
|
|
154
|
+
...deepInterviewQuestionState(question.question),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isAnswer(value: unknown): value is DeepInterviewGateAnswer {
|
|
161
|
+
if (typeof value !== "object" || value === null) return false;
|
|
162
|
+
const v = value as DeepInterviewGateAnswer;
|
|
163
|
+
return (
|
|
164
|
+
Array.isArray(v.selected) &&
|
|
165
|
+
v.selected.every(s => typeof s === "string") &&
|
|
166
|
+
(v.other === undefined || typeof v.other === "boolean") &&
|
|
167
|
+
(v.custom === undefined || typeof v.custom === "string")
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Decode a gate answer into the QuestionResult the interactive path produces.
|
|
173
|
+
* Selections are de-duplicated (the interactive UI stores them in a Set), and
|
|
174
|
+
* free text is taken from `other`/`custom`. Throws DeepInterviewGateError on a
|
|
175
|
+
* semantically invalid answer.
|
|
176
|
+
*/
|
|
177
|
+
export function gateAnswerToResult(question: AskGateQuestion, answer: unknown): AskGateResult {
|
|
178
|
+
if (!isAnswer(answer)) {
|
|
179
|
+
throw new DeepInterviewGateError(
|
|
180
|
+
"invalid_answer_shape",
|
|
181
|
+
"answer must be { selected: string[]; other?: boolean; custom?: string }",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const labels = question.options.map(o => o.label);
|
|
185
|
+
const multi = question.multi ?? false;
|
|
186
|
+
const valid = new Set(labels);
|
|
187
|
+
for (const sel of answer.selected) {
|
|
188
|
+
if (!valid.has(sel)) throw new DeepInterviewGateError("unknown_option", `unknown option: ${sel}`);
|
|
189
|
+
}
|
|
190
|
+
// Mirror the interactive UI, which stores selections in a Set (no duplicates).
|
|
191
|
+
const deduped = [...new Set(answer.selected)];
|
|
192
|
+
if (deduped.length !== answer.selected.length) {
|
|
193
|
+
throw new DeepInterviewGateError("duplicate_selection", "selected options must be unique");
|
|
194
|
+
}
|
|
195
|
+
const other = answer.other === true;
|
|
196
|
+
const totalPicks = deduped.length + (other ? 1 : 0);
|
|
197
|
+
if (totalPicks === 0) {
|
|
198
|
+
throw new DeepInterviewGateError(
|
|
199
|
+
"empty_selection",
|
|
200
|
+
"at least one option (or the free-text other) must be selected",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (!multi && totalPicks > 1) {
|
|
204
|
+
throw new DeepInterviewGateError("multi_not_allowed", "this question accepts a single selection");
|
|
205
|
+
}
|
|
206
|
+
if (other && (answer.custom === undefined || answer.custom.trim() === "")) {
|
|
207
|
+
throw new DeepInterviewGateError("missing_custom", "custom text is required when `other` is true");
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
id: question.id,
|
|
211
|
+
question: question.question,
|
|
212
|
+
options: labels,
|
|
213
|
+
multi,
|
|
214
|
+
selectedOptions: deduped,
|
|
215
|
+
customInput: other ? answer.custom : undefined,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Convenience: map a batch of ask questions to gate open-inputs. */
|
|
220
|
+
export function questionsToGates(questions: AskGateQuestion[]): OpenGateInput[] {
|
|
221
|
+
return questions.map(questionToGate);
|
|
222
|
+
}
|
|
@@ -106,3 +106,16 @@ export function toBridgeEventFrame(event: AgentSessionEvent, sequencer: BridgeFr
|
|
|
106
106
|
event,
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Serialize a `workflow_gate` event into a sequenced wire frame (#321). The
|
|
112
|
+
* gate_id is stamped as the correlation id so the answer (posted to the
|
|
113
|
+
* ui-responses endpoint) can be matched, and the monotonic `seq` gives replay
|
|
114
|
+
* while `frame_id` + gate_id give idempotency.
|
|
115
|
+
*/
|
|
116
|
+
export function toBridgeWorkflowGateFrame(
|
|
117
|
+
gate: import("../../rpc/rpc-types").RpcWorkflowGate,
|
|
118
|
+
sequencer: BridgeFrameSequencer,
|
|
119
|
+
): import("./protocol").BridgeWorkflowGateFrame {
|
|
120
|
+
return sequencer.next("workflow_gate", gate, gate.gate_id);
|
|
121
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RpcUnattendedDeclaration } from "../../rpc/rpc-types";
|
|
1
2
|
import { BRIDGE_PROTOCOL_VERSION, type BridgeFrameType } from "./protocol";
|
|
2
3
|
import type { BridgeCommandScope } from "./scopes";
|
|
3
4
|
|
|
@@ -13,7 +14,8 @@ export type BridgeCapability =
|
|
|
13
14
|
| "host_uri"
|
|
14
15
|
| "client_bridge.read_text_file"
|
|
15
16
|
| "client_bridge.write_text_file"
|
|
16
|
-
| "client_bridge.create_terminal"
|
|
17
|
+
| "client_bridge.create_terminal"
|
|
18
|
+
| "workflow_gate";
|
|
17
19
|
|
|
18
20
|
export interface BridgeProtocolRange {
|
|
19
21
|
min: number;
|
|
@@ -25,6 +27,8 @@ export interface BridgeHandshakeRequest {
|
|
|
25
27
|
capabilities: BridgeCapability[];
|
|
26
28
|
requested_scopes: BridgeCommandScope[];
|
|
27
29
|
last_seq?: number;
|
|
30
|
+
/** Optional unattended declaration (budget + scope + action allowlist) for #318/#319. */
|
|
31
|
+
unattended?: RpcUnattendedDeclaration;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export interface BridgeEndpointDescriptor {
|
|
@@ -46,6 +50,10 @@ export interface BridgeHandshakeAccepted {
|
|
|
46
50
|
unsupported: BridgeCapability[];
|
|
47
51
|
endpoints: BridgeEndpointDescriptor;
|
|
48
52
|
frame_types: BridgeFrameType[];
|
|
53
|
+
/** Echoed unattended declaration when one was supplied and accepted (#321). */
|
|
54
|
+
accepted_unattended?: RpcUnattendedDeclaration;
|
|
55
|
+
/** Server-side accepted unattended mode after live negotiation, not just declaration echo. */
|
|
56
|
+
unattended_active?: boolean;
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
export interface BridgeHandshakeRejected {
|
|
@@ -55,6 +63,29 @@ export interface BridgeHandshakeRejected {
|
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
export type BridgeHandshakeResponse = BridgeHandshakeAccepted | BridgeHandshakeRejected;
|
|
66
|
+
|
|
67
|
+
/** Shape-validate an optional unattended declaration carried on the handshake. */
|
|
68
|
+
export function isUnattendedDeclarationShape(value: unknown): value is RpcUnattendedDeclaration {
|
|
69
|
+
if (!value || typeof value !== "object") return false;
|
|
70
|
+
const d = value as Record<string, unknown>;
|
|
71
|
+
const b = d.budget as Record<string, unknown> | undefined;
|
|
72
|
+
const budgetOk =
|
|
73
|
+
!!b &&
|
|
74
|
+
typeof b === "object" &&
|
|
75
|
+
// Match UnattendedRunController fail-closed validation: positive finite budgets.
|
|
76
|
+
["max_tokens", "max_tool_calls", "max_wall_time_ms", "max_cost_usd"].every(
|
|
77
|
+
k => typeof b[k] === "number" && Number.isFinite(b[k] as number) && (b[k] as number) > 0,
|
|
78
|
+
);
|
|
79
|
+
return (
|
|
80
|
+
typeof d.actor === "string" &&
|
|
81
|
+
d.actor.trim() !== "" &&
|
|
82
|
+
budgetOk &&
|
|
83
|
+
Array.isArray(d.scopes) &&
|
|
84
|
+
d.scopes.every(s => typeof s === "string") &&
|
|
85
|
+
Array.isArray(d.action_allowlist) &&
|
|
86
|
+
d.action_allowlist.every(s => typeof s === "string")
|
|
87
|
+
);
|
|
88
|
+
}
|
|
58
89
|
export function isBridgeHandshakeRequest(value: unknown): value is BridgeHandshakeRequest {
|
|
59
90
|
if (!value || typeof value !== "object") return false;
|
|
60
91
|
const request = value as {
|
|
@@ -62,6 +93,7 @@ export function isBridgeHandshakeRequest(value: unknown): value is BridgeHandsha
|
|
|
62
93
|
capabilities?: unknown;
|
|
63
94
|
requested_scopes?: unknown;
|
|
64
95
|
last_seq?: unknown;
|
|
96
|
+
unattended?: unknown;
|
|
65
97
|
};
|
|
66
98
|
const range = request.protocol_version_range as { min?: unknown; max?: unknown } | undefined;
|
|
67
99
|
return (
|
|
@@ -74,7 +106,9 @@ export function isBridgeHandshakeRequest(value: unknown): value is BridgeHandsha
|
|
|
74
106
|
request.capabilities.every(capability => typeof capability === "string") &&
|
|
75
107
|
Array.isArray(request.requested_scopes) &&
|
|
76
108
|
request.requested_scopes.every(scope => typeof scope === "string") &&
|
|
77
|
-
(request.last_seq === undefined ||
|
|
109
|
+
(request.last_seq === undefined ||
|
|
110
|
+
(typeof request.last_seq === "number" && Number.isInteger(request.last_seq))) &&
|
|
111
|
+
(request.unattended === undefined || isUnattendedDeclarationShape(request.unattended))
|
|
78
112
|
);
|
|
79
113
|
}
|
|
80
114
|
|
|
@@ -86,6 +120,7 @@ export function negotiateBridgeHandshake(
|
|
|
86
120
|
scopes: readonly BridgeCommandScope[];
|
|
87
121
|
endpoints: BridgeEndpointDescriptor;
|
|
88
122
|
frameTypes: readonly BridgeFrameType[];
|
|
123
|
+
acceptedUnattended?: RpcUnattendedDeclaration;
|
|
89
124
|
},
|
|
90
125
|
): BridgeHandshakeResponse {
|
|
91
126
|
if (
|
|
@@ -104,7 +139,7 @@ export function negotiateBridgeHandshake(
|
|
|
104
139
|
const unsupported = request.capabilities.filter(capability => !acceptedSet.has(capability));
|
|
105
140
|
const serverScopes = new Set(server.scopes);
|
|
106
141
|
const acceptedScopes = request.requested_scopes.filter(scope => serverScopes.has(scope));
|
|
107
|
-
|
|
142
|
+
const accepted: BridgeHandshakeAccepted = {
|
|
108
143
|
status: "accepted",
|
|
109
144
|
protocol_version: BRIDGE_PROTOCOL_VERSION,
|
|
110
145
|
session_id: server.sessionId,
|
|
@@ -114,4 +149,9 @@ export function negotiateBridgeHandshake(
|
|
|
114
149
|
endpoints: server.endpoints,
|
|
115
150
|
frame_types: [...server.frameTypes],
|
|
116
151
|
};
|
|
152
|
+
if (server.acceptedUnattended !== undefined && acceptedSet.has("workflow_gate")) {
|
|
153
|
+
accepted.accepted_unattended = server.acceptedUnattended;
|
|
154
|
+
accepted.unattended_active = true;
|
|
155
|
+
}
|
|
156
|
+
return accepted;
|
|
117
157
|
}
|
|
@@ -64,6 +64,7 @@ export type BridgeFrameType =
|
|
|
64
64
|
| "host_tool_call"
|
|
65
65
|
| "host_uri_request"
|
|
66
66
|
| "reset"
|
|
67
|
+
| "workflow_gate"
|
|
67
68
|
| "error";
|
|
68
69
|
|
|
69
70
|
/**
|
|
@@ -94,3 +95,9 @@ export interface BridgeEventPayload {
|
|
|
94
95
|
|
|
95
96
|
/** An `AgentSessionEvent` serialized into a versioned wire frame. */
|
|
96
97
|
export type BridgeEventFrame = BridgeFrameEnvelope<"event", BridgeEventPayload>;
|
|
98
|
+
|
|
99
|
+
/** A `workflow_gate` event serialized into a versioned wire frame (#321). */
|
|
100
|
+
export type BridgeWorkflowGateFrame = BridgeFrameEnvelope<
|
|
101
|
+
"workflow_gate",
|
|
102
|
+
import("../../rpc/rpc-types").RpcWorkflowGate
|
|
103
|
+
>;
|
|
@@ -12,6 +12,6 @@ export function rpcSuccess<T extends RpcCommand["type"]>(
|
|
|
12
12
|
return { id, type: "response", command, success: true, data } as RpcResponse;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function rpcError(id: string | undefined, command: string,
|
|
16
|
-
return { id, type: "response", command, success: false, error
|
|
15
|
+
export function rpcError(id: string | undefined, command: string, error: string | object): RpcResponse {
|
|
16
|
+
return { id, type: "response", command, success: false, error } as RpcResponse;
|
|
17
17
|
}
|
|
@@ -71,6 +71,8 @@ const RPC_COMMAND_SCOPE_REGISTRY: Record<RpcCommandType, BridgeCommandScope> = {
|
|
|
71
71
|
get_messages: "message:read",
|
|
72
72
|
get_login_providers: "admin",
|
|
73
73
|
login: "admin",
|
|
74
|
+
negotiate_unattended: "control",
|
|
75
|
+
workflow_gate_response: "prompt",
|
|
74
76
|
};
|
|
75
77
|
|
|
76
78
|
export const RPC_COMMAND_TYPES: readonly RpcCommandType[] = Object.keys(RPC_COMMAND_SCOPE_REGISTRY) as RpcCommandType[];
|