@getmarrow/mcp 3.9.101 → 3.9.103
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/README.md +63 -29
- package/dist/cli.js +64 -1005
- package/dist/cli.js.map +1 -1
- package/dist/codex-plugin-pins.d.ts +19 -0
- package/dist/codex-plugin-pins.d.ts.map +1 -0
- package/dist/codex-plugin-pins.js +43 -0
- package/dist/codex-plugin-pins.js.map +1 -0
- package/dist/codex-plugins.d.ts +34 -0
- package/dist/codex-plugins.d.ts.map +1 -0
- package/dist/codex-plugins.js +240 -0
- package/dist/codex-plugins.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +229 -39
- package/dist/env.js.map +1 -1
- package/dist/hook-contract.d.ts +41 -3
- package/dist/hook-contract.d.ts.map +1 -1
- package/dist/hook-contract.js +126 -18
- package/dist/hook-contract.js.map +1 -1
- package/dist/hook-pre-action.d.ts +48 -2
- package/dist/hook-pre-action.d.ts.map +1 -1
- package/dist/hook-pre-action.js +199 -23
- package/dist/hook-pre-action.js.map +1 -1
- package/dist/hook-tool-policy.d.ts +3 -3
- package/dist/hook-tool-policy.d.ts.map +1 -1
- package/dist/hook-tool-policy.js +6 -22
- package/dist/hook-tool-policy.js.map +1 -1
- package/dist/hook.d.ts +2 -2
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +13 -6
- package/dist/hook.js.map +1 -1
- package/dist/host-approval-state.d.ts +9 -0
- package/dist/host-approval-state.d.ts.map +1 -1
- package/dist/host-approval-state.js +4 -0
- package/dist/host-approval-state.js.map +1 -1
- package/dist/host-approval.d.ts +6 -0
- package/dist/host-approval.d.ts.map +1 -1
- package/dist/host-approval.js +3 -1
- package/dist/host-approval.js.map +1 -1
- package/dist/host-session.d.ts +11 -0
- package/dist/host-session.d.ts.map +1 -1
- package/dist/host-session.js +49 -0
- package/dist/host-session.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -19
- package/dist/index.js.map +1 -1
- package/dist/normalized-action.d.ts +29 -6
- package/dist/normalized-action.d.ts.map +1 -1
- package/dist/normalized-action.js +497 -103
- package/dist/normalized-action.js.map +1 -1
- package/dist/own-server-identity.d.ts +123 -0
- package/dist/own-server-identity.d.ts.map +1 -0
- package/dist/own-server-identity.js +1052 -0
- package/dist/own-server-identity.js.map +1 -0
- package/dist/own-tool.d.ts +87 -0
- package/dist/own-tool.d.ts.map +1 -0
- package/dist/own-tool.js +169 -0
- package/dist/own-tool.js.map +1 -0
- package/dist/redact.d.ts +23 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +181 -8
- package/dist/redact.js.map +1 -1
- package/dist/seatbelt/classifier-pin.d.ts +15 -0
- package/dist/seatbelt/classifier-pin.d.ts.map +1 -0
- package/dist/seatbelt/classifier-pin.js +26 -0
- package/dist/seatbelt/classifier-pin.js.map +1 -0
- package/dist/seatbelt/guards.d.ts +10 -0
- package/dist/seatbelt/guards.d.ts.map +1 -0
- package/dist/seatbelt/guards.js +509 -0
- package/dist/seatbelt/guards.js.map +1 -0
- package/dist/seatbelt/hook.d.ts +72 -0
- package/dist/seatbelt/hook.d.ts.map +1 -0
- package/dist/seatbelt/hook.js +362 -0
- package/dist/seatbelt/hook.js.map +1 -0
- package/dist/seatbelt/longopts.d.ts +68 -0
- package/dist/seatbelt/longopts.d.ts.map +1 -0
- package/dist/seatbelt/longopts.js +377 -0
- package/dist/seatbelt/longopts.js.map +1 -0
- package/dist/seatbelt/mapper.d.ts +83 -0
- package/dist/seatbelt/mapper.d.ts.map +1 -0
- package/dist/seatbelt/mapper.js +10248 -0
- package/dist/seatbelt/mapper.js.map +1 -0
- package/dist/seatbelt/record.d.ts +94 -0
- package/dist/seatbelt/record.d.ts.map +1 -0
- package/dist/seatbelt/record.js +776 -0
- package/dist/seatbelt/record.js.map +1 -0
- package/dist/seatbelt/tables.d.ts +82 -0
- package/dist/seatbelt/tables.d.ts.map +1 -0
- package/dist/seatbelt/tables.js +359 -0
- package/dist/seatbelt/tables.js.map +1 -0
- package/dist/seatbelt-deploy.d.ts +100 -0
- package/dist/seatbelt-deploy.d.ts.map +1 -0
- package/dist/seatbelt-deploy.js +393 -0
- package/dist/seatbelt-deploy.js.map +1 -0
- package/dist/seatbelt-fs.d.ts +30 -0
- package/dist/seatbelt-fs.d.ts.map +1 -0
- package/dist/seatbelt-fs.js +94 -0
- package/dist/seatbelt-fs.js.map +1 -0
- package/dist/seatbelt-git.d.ts +212 -0
- package/dist/seatbelt-git.d.ts.map +1 -0
- package/dist/seatbelt-git.js +844 -0
- package/dist/seatbelt-git.js.map +1 -0
- package/dist/seatbelt-mode.d.ts +77 -0
- package/dist/seatbelt-mode.d.ts.map +1 -0
- package/dist/seatbelt-mode.js +496 -0
- package/dist/seatbelt-mode.js.map +1 -0
- package/dist/seatbelt-path.d.ts +123 -0
- package/dist/seatbelt-path.d.ts.map +1 -0
- package/dist/seatbelt-path.js +482 -0
- package/dist/seatbelt-path.js.map +1 -0
- package/dist/seatbelt-scripts.d.ts +109 -0
- package/dist/seatbelt-scripts.d.ts.map +1 -0
- package/dist/seatbelt-scripts.js +1060 -0
- package/dist/seatbelt-scripts.js.map +1 -0
- package/dist/tool-catalog.d.ts +6852 -0
- package/dist/tool-catalog.d.ts.map +1 -0
- package/dist/tool-catalog.js +999 -0
- package/dist/tool-catalog.js.map +1 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/server.json +10 -3
- package/vendor/classifier/MANIFEST.json +25 -0
- package/vendor/classifier/classifier.js +1356 -0
- package/vendor/classifier/policy-baseline.js +187 -0
- package/vendor/classifier/redact.js +59 -0
- package/vendor/classifier/shell-parse.js +455 -0
|
@@ -3,6 +3,10 @@ import { type HoldPlan } from './host-approval';
|
|
|
3
3
|
import { MARROW_OUTAGE_WARNING } from './hook-contract';
|
|
4
4
|
/** A protected action with no Marrow key anywhere this machine keeps one (MARROW_API_KEY, ~/.marrow/env.local or ~/.marrow/env, owner-only). */
|
|
5
5
|
export declare const NO_KEY_TEXT = "Marrow has no API key on this machine. Owner: create one at getmarrow.ai/signup, then run the installer activate command in a terminal. Do not retry this action.";
|
|
6
|
+
/** Seatbelt mode `claimed_no_key` (spec §1.1): account evidence on the machine but no usable key. Same text as src/seatbelt/hook.ts. */
|
|
7
|
+
export declare const CLAIMED_NO_KEY_TEXT = "Marrow is linked to an account but has no working key; owner: run `npx -y @getmarrow/install@latest doctor`.";
|
|
8
|
+
/** `claimed_no_key` without account evidence (a mode fallback): the same holds, never an account claim. Same text as src/seatbelt/hook.ts. */
|
|
9
|
+
export declare const SETUP_UNCONFIRMED_TEXT = "Marrow has no working key on this machine and its keyless mode is off here (strict mode, or a home folder it cannot trust); owner: run `npx -y @getmarrow/install@latest doctor`.";
|
|
6
10
|
export { MARROW_OUTAGE_WARNING };
|
|
7
11
|
export declare const MAX_PRE_ACTION_INPUT_BYTES: number;
|
|
8
12
|
export declare const PRE_ACTION_CONTROL_TIMEOUT_MS = 8000;
|
|
@@ -33,6 +37,8 @@ type HookHarness = 'claude-code' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'gr
|
|
|
33
37
|
export declare const HOLD_OWNER_DENIAL = "Marrow is holding this action for approval. Retry it after approval.";
|
|
34
38
|
/** Marrow did not answer within the hook's time limit: an action that can be held stays held. */
|
|
35
39
|
export declare const HELD_SLOW_TEXT = "Marrow did not answer in time, so this action is held. Retry it in a moment.";
|
|
40
|
+
/** Marrow cannot be reached (a real outage): a high-risk action fails closed; routine actions keep the outage allow. */
|
|
41
|
+
export declare const HELD_OFFLINE_TEXT = "Marrow cannot be reached right now, so this high-risk action is held; routine actions keep running. Retry it when Marrow is back.";
|
|
36
42
|
/** The only denial @getmarrow/install's Grok guard passes through (any other output blocks with a launch failure). */
|
|
37
43
|
export declare const GROK_FIXED_DENIAL = "Marrow blocked this protected action.";
|
|
38
44
|
/**
|
|
@@ -63,7 +69,7 @@ type PreActionControlResult = {
|
|
|
63
69
|
};
|
|
64
70
|
export declare function isMarrowOutage(result: PreActionControlResult): boolean;
|
|
65
71
|
export declare function controlFailureKind(error: unknown): PreActionControlResult['failure'];
|
|
66
|
-
export declare function controlRejectionMessage(error: unknown, agentId?: string): string;
|
|
72
|
+
export declare function controlRejectionMessage(error: unknown, agentId?: string, kind?: PreActionControlResult['failure']): string;
|
|
67
73
|
export type OwnerApprovalPrompt = {
|
|
68
74
|
available: boolean;
|
|
69
75
|
unavailableReason: string;
|
|
@@ -97,8 +103,37 @@ export declare function advisoryGateNotice(runtime: PreActionControlResult['runt
|
|
|
97
103
|
export declare function runtimeGateVerdict(runtime: PreActionControlResult['runtime']): GateVerdict | null;
|
|
98
104
|
export declare function gateDecisionMessage(verdict: GateVerdict, ask: boolean, prompt?: OwnerApprovalPrompt): string;
|
|
99
105
|
export declare function localControlAllowOutput(harness: 'claude-code' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'grok' | 'windsurf' | 'mcp-client'): Record<string, unknown> | null;
|
|
106
|
+
/** Keyless seatbelt: a host that cannot hand this action to its own approval prompt holds it (no account to approve it). */
|
|
107
|
+
export declare const SEATBELT_HELD_TEXT = "MARROW SEATBELT held this action: this host cannot hand it to its own approval prompt here, and no Marrow account is linked to approve it. This needs the machine owner. Do not retry it another way or edit Marrow settings. Tell the owner: run it yourself in your own terminal, or link a Marrow account with \"npx -y @getmarrow/install@latest activate\" (it needs MARROW_API_KEY set first) so these become approval prompts.";
|
|
108
|
+
export declare const SEATBELT_CURSOR_ASK_USER_TEXT = "Marrow seatbelt (no account linked): approve this action here only if you expected it.";
|
|
109
|
+
export declare const SEATBELT_CURSOR_ASK_AGENT_TEXT = "Cursor is asking the user to approve this action (Marrow seatbelt, no account linked). Wait for the answer; do not retry it another way.";
|
|
110
|
+
/**
|
|
111
|
+
* The keyless seatbelt's answer for an action it does not stop (spec §1.3 "pass": Marrow adds no
|
|
112
|
+
* decision and the host's own permission flow runs). `heldBefore`: the 3.9.103 keyless hook held this
|
|
113
|
+
* action (protected, or not read-only on Windsurf, Gemini or Grok). `null` means exit 0, no output.
|
|
114
|
+
* - Not held before: exactly the answer 3.9.103 gave (its read-only and unprotected passes).
|
|
115
|
+
* - Held before: never an explicit allow that could skip the host's own approval step.
|
|
116
|
+
* Claude Code and Codex `{}` (no permissionDecision), Cline `{cancel:false}`, Windsurf no output:
|
|
117
|
+
* the hosts' neutral answers (see PASS_THROUGH_HOSTS), so their own approval step runs.
|
|
118
|
+
* Gemini CLI `{"decision":"allow"}`: the installed guard passes only that or a fixed denial (no
|
|
119
|
+
* output is a launch failure that blocks), and Gemini applies its own policy and confirmation
|
|
120
|
+
* after the BeforeTool hook whatever an "allow" says, so it is Gemini's neutral answer.
|
|
121
|
+
* Cursor beforeShellExecution / beforeMCPExecution: "ask", so Cursor's own approval prompt decides.
|
|
122
|
+
* Cursor preToolUse (no neutral answer, "ask" not enforced) and Grok (its guard passes only an
|
|
123
|
+
* explicit allow or the fixed denial, and nothing shows an allow keeps Grok's own approval): held,
|
|
124
|
+
* as 3.9.103 did. Any other or missing Cursor event name is treated as preToolUse.
|
|
125
|
+
*/
|
|
126
|
+
export declare function seatbeltPassOutput(harness: 'claude-code' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'grok' | 'windsurf' | 'mcp-client', hookEventName: unknown, heldBefore: boolean): Record<string, unknown> | null;
|
|
100
127
|
export declare function localLoopGuardDenyOutput(harness: 'claude-code' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'grok' | 'windsurf' | 'mcp-client', reason: string): Record<string, unknown> | null;
|
|
101
|
-
|
|
128
|
+
/**
|
|
129
|
+
* `ownUnproven`: the label is one of Marrow's own, but the host's server
|
|
130
|
+
* identity check did not prove the official server (Codex, Cursor). Such a
|
|
131
|
+
* call is classified as a protected MCP tool of another server, never
|
|
132
|
+
* read-only (it imitates Marrow's names, so a read verb proves nothing).
|
|
133
|
+
*/
|
|
134
|
+
export declare function classifyTool(event: PreToolUseEvent, options?: {
|
|
135
|
+
ownUnproven?: boolean;
|
|
136
|
+
}): {
|
|
102
137
|
action: string;
|
|
103
138
|
target: string;
|
|
104
139
|
type: string;
|
|
@@ -145,5 +180,16 @@ export declare function installPreActionHook(startDir?: string): {
|
|
|
145
180
|
settingsPath: string;
|
|
146
181
|
installed: boolean;
|
|
147
182
|
};
|
|
183
|
+
/**
|
|
184
|
+
* I3: whether a real outage (the gate never answered) holds this action. Never looser than master,
|
|
185
|
+
* which allowed every action in an outage. A shell command is read by the seatbelt mapper: held for an
|
|
186
|
+
* S0-S5/S-x hit or a program on the outage list (git push, publish, deploy, remote delete), allowed
|
|
187
|
+
* when the mapper read all of it and found neither (words in a message, a path or a comment never hold
|
|
188
|
+
* it). Anything else, or a mapper that could not finish, keeps `classifiedHighRisk` (F2).
|
|
189
|
+
*/
|
|
190
|
+
export declare function outageHolds(classifiedHighRisk: boolean, read: () => {
|
|
191
|
+
shell: boolean;
|
|
192
|
+
risk: 'high' | 'not_high' | 'unknown';
|
|
193
|
+
}): boolean;
|
|
148
194
|
export declare function runPreActionHookCommand(input?: unknown): Promise<void>;
|
|
149
195
|
//# sourceMappingURL=hook-pre-action.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hook-pre-action.d.ts","sourceRoot":"","sources":["../src/hook-pre-action.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAgB,iBAAiB,EAAgC,MAAM,SAAS,CAAC;AAK5G,OAAO,EAsBL,KAAK,QAAQ,EAEd,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"hook-pre-action.d.ts","sourceRoot":"","sources":["../src/hook-pre-action.ts"],"names":[],"mappings":"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"}
|
package/dist/hook-pre-action.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DENIED_DECISION_CLOSE_TIMEOUT_MS = exports.GROK_FIXED_DENIAL = exports.HELD_SLOW_TEXT = exports.HOLD_OWNER_DENIAL = exports.PreActionControlTimeoutError = exports.PRE_ACTION_CONTROL_TIMEOUT_MS = exports.MAX_PRE_ACTION_INPUT_BYTES = exports.MARROW_OUTAGE_WARNING = exports.NO_KEY_TEXT = void 0;
|
|
3
|
+
exports.DENIED_DECISION_CLOSE_TIMEOUT_MS = exports.SEATBELT_CURSOR_ASK_AGENT_TEXT = exports.SEATBELT_CURSOR_ASK_USER_TEXT = exports.SEATBELT_HELD_TEXT = exports.GROK_FIXED_DENIAL = exports.HELD_OFFLINE_TEXT = exports.HELD_SLOW_TEXT = exports.HOLD_OWNER_DENIAL = exports.PreActionControlTimeoutError = exports.PRE_ACTION_CONTROL_TIMEOUT_MS = exports.MAX_PRE_ACTION_INPUT_BYTES = exports.MARROW_OUTAGE_WARNING = exports.SETUP_UNCONFIRMED_TEXT = exports.CLAIMED_NO_KEY_TEXT = exports.NO_KEY_TEXT = void 0;
|
|
4
4
|
exports.isMarrowControlTimeout = isMarrowControlTimeout;
|
|
5
5
|
exports.isMarrowControlOutage = isMarrowControlOutage;
|
|
6
6
|
exports.heldActionHookOutput = heldActionHookOutput;
|
|
@@ -16,6 +16,7 @@ exports.advisoryGateNotice = advisoryGateNotice;
|
|
|
16
16
|
exports.runtimeGateVerdict = runtimeGateVerdict;
|
|
17
17
|
exports.gateDecisionMessage = gateDecisionMessage;
|
|
18
18
|
exports.localControlAllowOutput = localControlAllowOutput;
|
|
19
|
+
exports.seatbeltPassOutput = seatbeltPassOutput;
|
|
19
20
|
exports.localLoopGuardDenyOutput = localLoopGuardDenyOutput;
|
|
20
21
|
exports.classifyTool = classifyTool;
|
|
21
22
|
exports.cursorPreActionHookOutput = cursorPreActionHookOutput;
|
|
@@ -27,6 +28,7 @@ exports.preActionHookOutput = preActionHookOutput;
|
|
|
27
28
|
exports.closeDeniedDecision = closeDeniedDecision;
|
|
28
29
|
exports.localApprovalCategories = localApprovalCategories;
|
|
29
30
|
exports.installPreActionHook = installPreActionHook;
|
|
31
|
+
exports.outageHolds = outageHolds;
|
|
30
32
|
exports.runPreActionHookCommand = runPreActionHookCommand;
|
|
31
33
|
const node_crypto_1 = require("node:crypto");
|
|
32
34
|
const index_1 = require("./index");
|
|
@@ -36,6 +38,7 @@ const control_state_1 = require("./control-state");
|
|
|
36
38
|
const runtime_contract_1 = require("./runtime-contract");
|
|
37
39
|
const host_approval_1 = require("./host-approval");
|
|
38
40
|
const host_session_1 = require("./host-session");
|
|
41
|
+
const own_server_identity_1 = require("./own-server-identity");
|
|
39
42
|
const normalized_action_1 = require("./normalized-action");
|
|
40
43
|
const session_loop_guard_1 = require("./session-loop-guard");
|
|
41
44
|
const hook_tool_policy_1 = require("./hook-tool-policy");
|
|
@@ -43,6 +46,12 @@ const hook_contract_1 = require("./hook-contract");
|
|
|
43
46
|
Object.defineProperty(exports, "MARROW_OUTAGE_WARNING", { enumerable: true, get: function () { return hook_contract_1.MARROW_OUTAGE_WARNING; } });
|
|
44
47
|
/** A protected action with no Marrow key anywhere this machine keeps one (MARROW_API_KEY, ~/.marrow/env.local or ~/.marrow/env, owner-only). */
|
|
45
48
|
exports.NO_KEY_TEXT = 'Marrow has no API key on this machine. Owner: create one at getmarrow.ai/signup, then run the installer activate command in a terminal. Do not retry this action.';
|
|
49
|
+
/** Seatbelt mode `claimed_no_key` (spec §1.1): account evidence on the machine but no usable key. Same text as src/seatbelt/hook.ts. */
|
|
50
|
+
exports.CLAIMED_NO_KEY_TEXT = 'Marrow is linked to an account but has no working key; owner: run `npx -y @getmarrow/install@latest doctor`.';
|
|
51
|
+
/** `claimed_no_key` without account evidence (a mode fallback): the same holds, never an account claim. Same text as src/seatbelt/hook.ts. */
|
|
52
|
+
exports.SETUP_UNCONFIRMED_TEXT = 'Marrow has no working key on this machine and its keyless mode is off here (strict mode, or a home folder it cannot trust); owner: run `npx -y @getmarrow/install@latest doctor`.';
|
|
53
|
+
/** The seatbelt path itself could not run (its modules did not load): a stop (spec §2.1.4). */
|
|
54
|
+
const SEATBELT_FAILED_TEXT = 'MARROW SEATBELT stopped this action: a safety check that could not finish (U5.fail_closed, hook_error). This needs the machine owner. Do not retry it another way or edit Marrow settings.';
|
|
46
55
|
// Claude Code sends the whole tool input, and a Write of a long document grows
|
|
47
56
|
// further once JSON-escaped, so tens of kilobytes of markdown must still parse.
|
|
48
57
|
exports.MAX_PRE_ACTION_INPUT_BYTES = 4 * 1024 * 1024;
|
|
@@ -107,6 +116,8 @@ function isMarrowControlOutage(error) {
|
|
|
107
116
|
exports.HOLD_OWNER_DENIAL = 'Marrow is holding this action for approval. Retry it after approval.';
|
|
108
117
|
/** Marrow did not answer within the hook's time limit: an action that can be held stays held. */
|
|
109
118
|
exports.HELD_SLOW_TEXT = 'Marrow did not answer in time, so this action is held. Retry it in a moment.';
|
|
119
|
+
/** Marrow cannot be reached (a real outage): a high-risk action fails closed; routine actions keep the outage allow. */
|
|
120
|
+
exports.HELD_OFFLINE_TEXT = 'Marrow cannot be reached right now, so this high-risk action is held; routine actions keep running. Retry it when Marrow is back.';
|
|
110
121
|
/** The only denial @getmarrow/install's Grok guard passes through (any other output blocks with a launch failure). */
|
|
111
122
|
exports.GROK_FIXED_DENIAL = 'Marrow blocked this protected action.';
|
|
112
123
|
/**
|
|
@@ -231,7 +242,7 @@ function plainControlFix(error) {
|
|
|
231
242
|
}
|
|
232
243
|
// Names a reached control failure by HTTP status and stable failure code, with a plain
|
|
233
244
|
// fix (see plainControlFix); private service text is never echoed into the agent transcript.
|
|
234
|
-
function controlRejectionMessage(error, agentId) {
|
|
245
|
+
function controlRejectionMessage(error, agentId, kind = controlFailureKind(error)) {
|
|
235
246
|
const detail = [];
|
|
236
247
|
if (error instanceof request_reliability_1.MarrowRequestError) {
|
|
237
248
|
if (typeof error.status === 'number' && Number.isInteger(error.status))
|
|
@@ -241,7 +252,6 @@ function controlRejectionMessage(error, agentId) {
|
|
|
241
252
|
detail.push(code);
|
|
242
253
|
}
|
|
243
254
|
const suffix = detail.length ? ` (${detail.join(' ')})` : '';
|
|
244
|
-
const kind = controlFailureKind(error);
|
|
245
255
|
if (kind === 'credential_scope') {
|
|
246
256
|
const agent = agentId && SAFE_AGENT_ID.test(agentId)
|
|
247
257
|
? `agent "${agentId}"`
|
|
@@ -410,6 +420,46 @@ function localControlAllowOutput(harness) {
|
|
|
410
420
|
return { decision: 'allow' };
|
|
411
421
|
return {};
|
|
412
422
|
}
|
|
423
|
+
/** The Cursor events where Cursor enforces "ask" with its own approval prompt (the installer's execution hooks). */
|
|
424
|
+
const CURSOR_ASK_EVENTS = new Set(['beforeShellExecution', 'beforeMCPExecution']);
|
|
425
|
+
/** Keyless seatbelt: a host that cannot hand this action to its own approval prompt holds it (no account to approve it). */
|
|
426
|
+
exports.SEATBELT_HELD_TEXT = 'MARROW SEATBELT held this action: this host cannot hand it to its own approval prompt here, and no Marrow account is linked to approve it. This needs the machine owner. Do not retry it another way or edit Marrow settings. Tell the owner: run it yourself in your own terminal, or link a Marrow account with "npx -y @getmarrow/install@latest activate" (it needs MARROW_API_KEY set first) so these become approval prompts.';
|
|
427
|
+
exports.SEATBELT_CURSOR_ASK_USER_TEXT = 'Marrow seatbelt (no account linked): approve this action here only if you expected it.';
|
|
428
|
+
exports.SEATBELT_CURSOR_ASK_AGENT_TEXT = 'Cursor is asking the user to approve this action (Marrow seatbelt, no account linked). Wait for the answer; do not retry it another way.';
|
|
429
|
+
/**
|
|
430
|
+
* The keyless seatbelt's answer for an action it does not stop (spec §1.3 "pass": Marrow adds no
|
|
431
|
+
* decision and the host's own permission flow runs). `heldBefore`: the 3.9.103 keyless hook held this
|
|
432
|
+
* action (protected, or not read-only on Windsurf, Gemini or Grok). `null` means exit 0, no output.
|
|
433
|
+
* - Not held before: exactly the answer 3.9.103 gave (its read-only and unprotected passes).
|
|
434
|
+
* - Held before: never an explicit allow that could skip the host's own approval step.
|
|
435
|
+
* Claude Code and Codex `{}` (no permissionDecision), Cline `{cancel:false}`, Windsurf no output:
|
|
436
|
+
* the hosts' neutral answers (see PASS_THROUGH_HOSTS), so their own approval step runs.
|
|
437
|
+
* Gemini CLI `{"decision":"allow"}`: the installed guard passes only that or a fixed denial (no
|
|
438
|
+
* output is a launch failure that blocks), and Gemini applies its own policy and confirmation
|
|
439
|
+
* after the BeforeTool hook whatever an "allow" says, so it is Gemini's neutral answer.
|
|
440
|
+
* Cursor beforeShellExecution / beforeMCPExecution: "ask", so Cursor's own approval prompt decides.
|
|
441
|
+
* Cursor preToolUse (no neutral answer, "ask" not enforced) and Grok (its guard passes only an
|
|
442
|
+
* explicit allow or the fixed denial, and nothing shows an allow keeps Grok's own approval): held,
|
|
443
|
+
* as 3.9.103 did. Any other or missing Cursor event name is treated as preToolUse.
|
|
444
|
+
*/
|
|
445
|
+
function seatbeltPassOutput(harness, hookEventName, heldBefore) {
|
|
446
|
+
if (!heldBefore)
|
|
447
|
+
return localControlAllowOutput(harness);
|
|
448
|
+
if (harness === 'windsurf')
|
|
449
|
+
return null;
|
|
450
|
+
if (harness === 'cline')
|
|
451
|
+
return { cancel: false };
|
|
452
|
+
if (harness === 'gemini')
|
|
453
|
+
return { decision: 'allow' };
|
|
454
|
+
if (harness === 'grok')
|
|
455
|
+
return { decision: 'deny', reason: exports.GROK_FIXED_DENIAL };
|
|
456
|
+
if (harness === 'cursor') {
|
|
457
|
+
return typeof hookEventName === 'string' && CURSOR_ASK_EVENTS.has(hookEventName)
|
|
458
|
+
? { permission: 'ask', user_message: exports.SEATBELT_CURSOR_ASK_USER_TEXT, agent_message: exports.SEATBELT_CURSOR_ASK_AGENT_TEXT }
|
|
459
|
+
: { permission: 'deny', user_message: exports.SEATBELT_HELD_TEXT, agent_message: exports.SEATBELT_HELD_TEXT };
|
|
460
|
+
}
|
|
461
|
+
return {};
|
|
462
|
+
}
|
|
413
463
|
function localLoopGuardDenyOutput(harness, reason) {
|
|
414
464
|
const bounded = reason.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
415
465
|
if (harness === 'windsurf')
|
|
@@ -418,7 +468,10 @@ function localLoopGuardDenyOutput(harness, reason) {
|
|
|
418
468
|
return { permission: 'deny', user_message: bounded, agent_message: bounded };
|
|
419
469
|
if (harness === 'cline')
|
|
420
470
|
return { cancel: true, errorMessage: bounded };
|
|
421
|
-
|
|
471
|
+
// The installed Grok guard passes only an allow or this fixed denial; any other text is its launch failure (audit M1).
|
|
472
|
+
if (harness === 'grok')
|
|
473
|
+
return { decision: 'deny', reason: exports.GROK_FIXED_DENIAL };
|
|
474
|
+
if (harness === 'gemini')
|
|
422
475
|
return { decision: 'deny', reason: bounded };
|
|
423
476
|
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: bounded } };
|
|
424
477
|
}
|
|
@@ -456,7 +509,13 @@ function localControlDisabled() {
|
|
|
456
509
|
return false;
|
|
457
510
|
}
|
|
458
511
|
}
|
|
459
|
-
|
|
512
|
+
/**
|
|
513
|
+
* `ownUnproven`: the label is one of Marrow's own, but the host's server
|
|
514
|
+
* identity check did not prove the official server (Codex, Cursor). Such a
|
|
515
|
+
* call is classified as a protected MCP tool of another server, never
|
|
516
|
+
* read-only (it imitates Marrow's names, so a read verb proves nothing).
|
|
517
|
+
*/
|
|
518
|
+
function classifyTool(event, options = {}) {
|
|
460
519
|
const tool = String(event.tool_name || 'tool').slice(0, 64);
|
|
461
520
|
const normalizedTool = (0, hook_tool_policy_1.normalizeHookToolName)(tool);
|
|
462
521
|
// Editing and task tools are judged by their target; shell-like words in the
|
|
@@ -464,7 +523,8 @@ function classifyTool(event) {
|
|
|
464
523
|
const shellGoverned = (0, hook_tool_policy_1.isShellGovernedTool)(event);
|
|
465
524
|
const command = shellGoverned ? (0, hook_tool_policy_1.hookToolCommand)(event) : '';
|
|
466
525
|
const input = (0, hook_tool_policy_1.toolClassificationText)(event).toLowerCase();
|
|
467
|
-
|
|
526
|
+
// A look-alike of Marrow's own tool is never read-only, whatever its verb.
|
|
527
|
+
const readOnly = options.ownUnproven !== true && (0, hook_tool_policy_1.isReadOnlyToolEvent)(event);
|
|
468
528
|
const secretAccess = (0, hook_tool_policy_1.isSecretMaterialAccess)(event);
|
|
469
529
|
const protectedShellCommand = shellGoverned && (0, hook_tool_policy_1.isProtectedShellMutation)(command);
|
|
470
530
|
const infrastructureDeployment = /\b(?:kubectl|terraform|pulumi|helm)\b/.test(command.toLowerCase())
|
|
@@ -493,7 +553,7 @@ function classifyTool(event) {
|
|
|
493
553
|
const protectedAction = !readOnly && (secretAccess
|
|
494
554
|
|| (shellGoverned && /\b(?:deploy|release|publish|git\s+push|git\s+merge|gh\s+pr\s+merge|migration|migrate|secret|credential|rotate|revoke|payment|refund|charge|invoice|production|prod)\b/.test(input))
|
|
495
555
|
|| protectedShellCommand
|
|
496
|
-
|| ((0, hook_tool_policy_1.isMcpHookTool)(event.tool_name) && !(0, hook_tool_policy_1.isOfficialMarrowMcpTool)(event.tool_name))
|
|
556
|
+
|| ((0, hook_tool_policy_1.isMcpHookTool)(event.tool_name) && (options.ownUnproven === true || !(0, hook_tool_policy_1.isOfficialMarrowMcpTool)(event.tool_name)))
|
|
497
557
|
|| (['use_mcp_tool', 'use_tool'].includes(normalizedTool) && !(0, hook_tool_policy_1.isOfficialMarrowMcpEvent)(event)));
|
|
498
558
|
const risk = readOnly ? 'low' : protectedAction ? 'high' : 'medium';
|
|
499
559
|
const target = surfaces.includes('npm') ? `npm:${type}`
|
|
@@ -548,11 +608,12 @@ function clinePreActionHookOutput(result) {
|
|
|
548
608
|
};
|
|
549
609
|
}
|
|
550
610
|
if (result.protectedRisk && (!result.runtime || !result.permit?.verified)) {
|
|
551
|
-
const credentialsUnavailable = result.enforcementError === exports.NO_KEY_TEXT
|
|
611
|
+
const credentialsUnavailable = result.enforcementError === exports.NO_KEY_TEXT || result.enforcementError === exports.CLAIMED_NO_KEY_TEXT
|
|
612
|
+
|| result.enforcementError === exports.SETUP_UNCONFIRMED_TEXT;
|
|
552
613
|
return {
|
|
553
614
|
cancel: true,
|
|
554
615
|
errorMessage: credentialsUnavailable
|
|
555
|
-
?
|
|
616
|
+
? result.enforcementError
|
|
556
617
|
: result.failure === 'credential_scope'
|
|
557
618
|
? 'This Marrow API key is not authorized to obtain action permits for this agent. Use the API key issued to this agent and retry.'
|
|
558
619
|
: result.failure === 'unavailable'
|
|
@@ -828,11 +889,54 @@ function installPreActionHook(startDir = process.cwd()) {
|
|
|
828
889
|
fs.writeFileSync(path, JSON.stringify(settings, null, 2) + '\n');
|
|
829
890
|
return { settingsPath: path, installed: reconciled.changed };
|
|
830
891
|
}
|
|
892
|
+
/**
|
|
893
|
+
* I3: whether a real outage (the gate never answered) holds this action. Never looser than master,
|
|
894
|
+
* which allowed every action in an outage. A shell command is read by the seatbelt mapper: held for an
|
|
895
|
+
* S0-S5/S-x hit or a program on the outage list (git push, publish, deploy, remote delete), allowed
|
|
896
|
+
* when the mapper read all of it and found neither (words in a message, a path or a comment never hold
|
|
897
|
+
* it). Anything else, or a mapper that could not finish, keeps `classifiedHighRisk` (F2).
|
|
898
|
+
*/
|
|
899
|
+
function outageHolds(classifiedHighRisk, read) {
|
|
900
|
+
let reading = { shell: false, risk: 'unknown' };
|
|
901
|
+
try {
|
|
902
|
+
reading = read();
|
|
903
|
+
}
|
|
904
|
+
catch { /* unknown: the classifier's answer */ }
|
|
905
|
+
if (reading.risk === 'high')
|
|
906
|
+
return true;
|
|
907
|
+
if (reading.shell && reading.risk === 'not_high')
|
|
908
|
+
return false;
|
|
909
|
+
return classifiedHighRisk;
|
|
910
|
+
}
|
|
911
|
+
function seatbeltEvent(source, rawEvent) {
|
|
912
|
+
const raw = asRecord(rawEvent) || {};
|
|
913
|
+
const event = source;
|
|
914
|
+
return {
|
|
915
|
+
tool_name: source.tool_name, tool_input: source.tool_input, cwd: event.cwd ?? raw.cwd, truncated: event.input_truncated === true,
|
|
916
|
+
session: source.session_id || source.conversation_id || source.task_id || undefined,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
831
919
|
async function runPreActionHookCommand(input) {
|
|
920
|
+
// Account mode: the S0-S5 floor in watch runs after the answer is written, for the record only.
|
|
921
|
+
const watch = {};
|
|
922
|
+
try {
|
|
923
|
+
await runPreActionHook(input, watch);
|
|
924
|
+
}
|
|
925
|
+
finally {
|
|
926
|
+
if (watch.harness && watch.event) {
|
|
927
|
+
try {
|
|
928
|
+
require('./seatbelt/hook').accountFloor(watch.harness, watch.event, (0, host_approval_1.preToolDeadline)((0, host_approval_1.approvalHostFor)(watch.harness)));
|
|
929
|
+
}
|
|
930
|
+
catch { /* watch never changes the answer */ }
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async function runPreActionHook(input, watch) {
|
|
832
935
|
if (process.env.MARROW_AUTO_HOOK === 'false')
|
|
833
936
|
return;
|
|
834
937
|
const identity = (0, hook_contract_1.resolveNativeHookIdentity)(process.argv[2]);
|
|
835
938
|
let event = input;
|
|
939
|
+
let rawEvent = input;
|
|
836
940
|
let inputFailure = null;
|
|
837
941
|
if (event === undefined) {
|
|
838
942
|
try {
|
|
@@ -842,7 +946,8 @@ async function runPreActionHookCommand(input) {
|
|
|
842
946
|
}
|
|
843
947
|
else {
|
|
844
948
|
const raw = stdin.raw.trim();
|
|
845
|
-
|
|
949
|
+
rawEvent = raw ? JSON.parse(raw) : {};
|
|
950
|
+
event = (0, hook_contract_1.normalizeHookEventPayload)(rawEvent);
|
|
846
951
|
}
|
|
847
952
|
}
|
|
848
953
|
catch {
|
|
@@ -865,7 +970,7 @@ async function runPreActionHookCommand(input) {
|
|
|
865
970
|
emitDecision({ runtime: null, permit: null, protectedRisk: true, enforcementError: inputFailure || 'Marrow could not classify this mutation-capable tool request.' }, identity.harness);
|
|
866
971
|
return;
|
|
867
972
|
}
|
|
868
|
-
if ((0,
|
|
973
|
+
if ((0, own_server_identity_1.isOfficialMarrowMcpEventOnHost)(identity.harness, source, rawEvent)) {
|
|
869
974
|
if (identity.harness === 'windsurf') {
|
|
870
975
|
process.exitCode = 0;
|
|
871
976
|
return;
|
|
@@ -876,7 +981,7 @@ async function runPreActionHookCommand(input) {
|
|
|
876
981
|
: {}));
|
|
877
982
|
return;
|
|
878
983
|
}
|
|
879
|
-
const classified = classifyTool(source);
|
|
984
|
+
const classified = classifyTool(source, { ownUnproven: (0, hook_tool_policy_1.isOfficialMarrowMcpEvent)(source) });
|
|
880
985
|
let localControl;
|
|
881
986
|
try {
|
|
882
987
|
localControl = (0, control_state_1.readLocalControlState)();
|
|
@@ -962,6 +1067,55 @@ async function runPreActionHookCommand(input) {
|
|
|
962
1067
|
emitLoopGuardDenial(identity.harness, loopDecision.reason || `Marrow local loop guard blocked this unchanged repeat. Receipt: ${loopDecision.receipt}.`);
|
|
963
1068
|
return;
|
|
964
1069
|
}
|
|
1070
|
+
// No usable key: the seatbelt decides first (spec §1.1), before the read-only pass so S4 reads stop.
|
|
1071
|
+
// `seatbelt` answers here with no network call; `claimed_no_key` stops S0-S5 here and keeps the holds below.
|
|
1072
|
+
// Whether a keyless hold may say "linked to an account" (only with account evidence, never for a mode fallback).
|
|
1073
|
+
let keylessAccountLinked = false;
|
|
1074
|
+
// The keyless hold text when the seatbelt does not answer: master's no-key text in seatbelt mode (a
|
|
1075
|
+
// tool the mapper does not recognise, audit H1), the claimed_no_key texts otherwise.
|
|
1076
|
+
let keylessSeatbeltMode = false;
|
|
1077
|
+
if (!resolved.apiKey) {
|
|
1078
|
+
let keyless;
|
|
1079
|
+
try {
|
|
1080
|
+
keyless = require('./seatbelt/hook').keylessSeatbelt(identity.harness, seatbeltEvent(source, rawEvent));
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
keyless = { mode: 'claimed_no_key', stopText: SEATBELT_FAILED_TEXT };
|
|
1084
|
+
}
|
|
1085
|
+
keylessAccountLinked = keyless.accountLinked === true;
|
|
1086
|
+
keylessSeatbeltMode = keyless.mode === 'seatbelt';
|
|
1087
|
+
if (keyless.stopText) {
|
|
1088
|
+
emitLoopGuardDenial(identity.harness, keyless.stopText);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
// A tool the mapper does not recognise never gets the seatbelt pass: master's answer below.
|
|
1092
|
+
if (keyless.mode === 'seatbelt' && keyless.recognized === true) {
|
|
1093
|
+
// Never an explicit allow for an action 3.9.103 held: the host's own approval step decides.
|
|
1094
|
+
const answer = seatbeltPassOutput(identity.harness, source.hook_event_name, !classified.readOnly && enforcementRequired);
|
|
1095
|
+
if (answer === null)
|
|
1096
|
+
process.exitCode = 0;
|
|
1097
|
+
else
|
|
1098
|
+
process.stdout.write(JSON.stringify(answer));
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
watch.harness = identity.harness;
|
|
1104
|
+
watch.event = seatbeltEvent(source, rawEvent);
|
|
1105
|
+
let floor = null;
|
|
1106
|
+
try {
|
|
1107
|
+
floor = require('./seatbelt/hook');
|
|
1108
|
+
}
|
|
1109
|
+
catch { /* watch only: a missing module never changes the account flow */ }
|
|
1110
|
+
if (floor && floor.SEATBELT_ROLLOUT.account_floor === 'on') {
|
|
1111
|
+
watch.harness = undefined;
|
|
1112
|
+
const stopText = floor.accountFloor(identity.harness, watch.event, (0, host_approval_1.preToolDeadline)((0, host_approval_1.approvalHostFor)(identity.harness)));
|
|
1113
|
+
if (stopText) {
|
|
1114
|
+
emitLoopGuardDenial(identity.harness, stopText);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
965
1119
|
if (classified.readOnly) {
|
|
966
1120
|
const allow = localControlAllowOutput(identity.harness);
|
|
967
1121
|
if (allow === null)
|
|
@@ -988,7 +1142,7 @@ async function runPreActionHookCommand(input) {
|
|
|
988
1142
|
runtime: null,
|
|
989
1143
|
permit: null,
|
|
990
1144
|
protectedRisk: enforcementRequired,
|
|
991
|
-
enforcementError: exports.NO_KEY_TEXT,
|
|
1145
|
+
enforcementError: keylessSeatbeltMode ? exports.NO_KEY_TEXT : keylessAccountLinked ? exports.CLAIMED_NO_KEY_TEXT : exports.SETUP_UNCONFIRMED_TEXT,
|
|
992
1146
|
}, identity.harness);
|
|
993
1147
|
return;
|
|
994
1148
|
}
|
|
@@ -1017,7 +1171,9 @@ async function runPreActionHookCommand(input) {
|
|
|
1017
1171
|
}
|
|
1018
1172
|
// The same action retried after a hold that waited for approval: its status decides.
|
|
1019
1173
|
const dialogAvailable = holdContext.host === 'claude-code' && ownerApprovalPrompt(identity.harness, source).available;
|
|
1020
|
-
|
|
1174
|
+
// The exact target (not only the correlation) binds a hold's approval to this call.
|
|
1175
|
+
const target = (0, hook_contract_1.exactHookTarget)(source);
|
|
1176
|
+
const waited = await (0, host_approval_1.resumeWaitingHold)(holdContext, { correlation, target, toolUseId, generationId, dialogAvailable }).catch(() => null);
|
|
1021
1177
|
if (waited?.kind === 'allow') {
|
|
1022
1178
|
emitHookOutput(identity.harness, approvedHoldHookOutput(identity.harness, waited.contextText));
|
|
1023
1179
|
await afterAnswer(holdContext, {});
|
|
@@ -1056,6 +1212,11 @@ async function runPreActionHookCommand(input) {
|
|
|
1056
1212
|
});
|
|
1057
1213
|
// The decision and gate receipt this control path holds, so a denial can close them.
|
|
1058
1214
|
const held = { decisionId: null, gateReceiptId: null };
|
|
1215
|
+
// Set once the runtime gate has answered. A later network or 5xx failure (decision, permit issue
|
|
1216
|
+
// or verify) on a high-risk action is a reached answer that no permit can be had: a denial, never
|
|
1217
|
+
// "offline, allowed". Routine actions keep the outage policy (failure_mode low_risk_behavior).
|
|
1218
|
+
const reach = { gateAnswered: false };
|
|
1219
|
+
const highRisk = classified.protected || classified.risk === 'high';
|
|
1059
1220
|
const control = async (signal) => {
|
|
1060
1221
|
const runtime = await (0, index_1.marrowAgentRuntime)(resolved.apiKey, baseUrl, {
|
|
1061
1222
|
action: classified.action,
|
|
@@ -1070,6 +1231,7 @@ async function runPreActionHookCommand(input) {
|
|
|
1070
1231
|
// non-durable fast_gate receipt can never back an action permit.
|
|
1071
1232
|
...(enforcementRequired ? { risk_level: classified.risk } : {}),
|
|
1072
1233
|
}, sessionId, agentId, signal);
|
|
1234
|
+
reach.gateAnswered = true;
|
|
1073
1235
|
const heldIds = [...new Set([runtime.decision_id, runtime.completion_contract?.decision_id, runtime.runtime_authorization?.decision_id]
|
|
1074
1236
|
.filter((value) => typeof value === 'string' && SAFE_DECISION_ID.test(value)))];
|
|
1075
1237
|
held.decisionId = heldIds.length === 1 ? heldIds[0] : null;
|
|
@@ -1171,7 +1333,7 @@ async function runPreActionHookCommand(input) {
|
|
|
1171
1333
|
timedOut: true,
|
|
1172
1334
|
enforcementError: hook_contract_1.MARROW_OUTAGE_WARNING,
|
|
1173
1335
|
}
|
|
1174
|
-
: isMarrowControlOutage(error)
|
|
1336
|
+
: isMarrowControlOutage(error) && !(reach.gateAnswered && highRisk)
|
|
1175
1337
|
? {
|
|
1176
1338
|
runtime: null,
|
|
1177
1339
|
permit: null,
|
|
@@ -1179,13 +1341,21 @@ async function runPreActionHookCommand(input) {
|
|
|
1179
1341
|
outage: true,
|
|
1180
1342
|
enforcementError: hook_contract_1.MARROW_OUTAGE_WARNING,
|
|
1181
1343
|
}
|
|
1182
|
-
:
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1344
|
+
: isMarrowControlOutage(error)
|
|
1345
|
+
? {
|
|
1346
|
+
runtime: null,
|
|
1347
|
+
permit: null,
|
|
1348
|
+
protectedRisk: enforcementRequired,
|
|
1349
|
+
enforcementError: controlRejectionMessage(error, agentId, 'unavailable'),
|
|
1350
|
+
failure: 'unavailable',
|
|
1351
|
+
}
|
|
1352
|
+
: {
|
|
1353
|
+
runtime: null,
|
|
1354
|
+
permit: null,
|
|
1355
|
+
protectedRisk: enforcementRequired,
|
|
1356
|
+
enforcementError: controlRejectionMessage(error, agentId),
|
|
1357
|
+
...(controlFailureKind(error) ? { failure: controlFailureKind(error) } : {}),
|
|
1358
|
+
})),
|
|
1189
1359
|
lifecycle,
|
|
1190
1360
|
]);
|
|
1191
1361
|
const claudePrompt = ownerApprovalPrompt(identity.harness, source);
|
|
@@ -1203,12 +1373,17 @@ async function runPreActionHookCommand(input) {
|
|
|
1203
1373
|
}
|
|
1204
1374
|
if (result.outage) {
|
|
1205
1375
|
// Marrow cannot be reached. An action in a category the account owner
|
|
1206
|
-
// protects stays held on every host;
|
|
1376
|
+
// protects stays held on every host; a high-risk action fails closed (high-risk
|
|
1377
|
+
// work needs a fresh gate); routine actions keep the outage policy.
|
|
1207
1378
|
const protectedNow = (0, host_approval_1.protectedAmong)(holdContext, localApprovalCategories(classified));
|
|
1208
1379
|
if (protectedNow.length) {
|
|
1209
1380
|
emitHeldPlan(identity.harness, { kind: 'deny', agentText: host_approval_1.HELD_UNREACHABLE_TEXT, userText: host_approval_1.HELD_UNREACHABLE_TEXT, code: false });
|
|
1210
1381
|
return;
|
|
1211
1382
|
}
|
|
1383
|
+
if (outageHolds(highRisk, () => require('./seatbelt/hook').outageRisk(seatbeltEvent(source, rawEvent), holdContext.deadlineAt ?? Number.POSITIVE_INFINITY))) {
|
|
1384
|
+
emitHeldPlan(identity.harness, { kind: 'deny', agentText: exports.HELD_OFFLINE_TEXT, userText: exports.HELD_OFFLINE_TEXT, code: false });
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1212
1387
|
}
|
|
1213
1388
|
const verdict = result.outage ? null : runtimeGateVerdict(result.runtime);
|
|
1214
1389
|
const guidance = verdict?.kind === 'review' ? (0, runtime_contract_1.ordinaryApprovalGuidance)(result.runtime) : null;
|
|
@@ -1258,6 +1433,7 @@ async function runPreActionHookCommand(input) {
|
|
|
1258
1433
|
const hold = plan.kind === 'deny' && plan.retryFresh ? null : (0, host_approval_1.rememberHold)(holdContext, {
|
|
1259
1434
|
guidance: guidance || (0, host_approval_1.arbitrationHoldGuidance)(arbitration),
|
|
1260
1435
|
correlation,
|
|
1436
|
+
target,
|
|
1261
1437
|
toolUseId,
|
|
1262
1438
|
generationId,
|
|
1263
1439
|
toolName: String(source.tool_name || 'tool'),
|