@getmarrow/mcp 3.9.98 → 3.9.99
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 +57 -342
- package/dist/cli.js +249 -47
- package/dist/cli.js.map +1 -1
- package/dist/hook-context.d.ts.map +1 -1
- package/dist/hook-context.js +135 -9
- package/dist/hook-context.js.map +1 -1
- package/dist/hook-contract.d.ts +26 -5
- package/dist/hook-contract.d.ts.map +1 -1
- package/dist/hook-contract.js +144 -10
- package/dist/hook-contract.js.map +1 -1
- package/dist/hook-pre-action.d.ts +40 -1
- package/dist/hook-pre-action.d.ts.map +1 -1
- package/dist/hook-pre-action.js +429 -35
- package/dist/hook-pre-action.js.map +1 -1
- package/dist/hook-session.d.ts.map +1 -1
- package/dist/hook-session.js +32 -0
- package/dist/hook-session.js.map +1 -1
- package/dist/hook.d.ts +12 -0
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +149 -5
- package/dist/hook.js.map +1 -1
- package/dist/host-approval-state.d.ts +137 -0
- package/dist/host-approval-state.d.ts.map +1 -0
- package/dist/host-approval-state.js +510 -0
- package/dist/host-approval-state.js.map +1 -0
- package/dist/host-approval.d.ts +436 -0
- package/dist/host-approval.d.ts.map +1 -0
- package/dist/host-approval.js +1417 -0
- package/dist/host-approval.js.map +1 -0
- package/dist/host-session.d.ts +82 -0
- package/dist/host-session.d.ts.map +1 -0
- package/dist/host-session.js +570 -0
- package/dist/host-session.js.map +1 -0
- package/dist/index.d.ts +150 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +748 -33
- package/dist/index.js.map +1 -1
- package/dist/lifecycle-spool.d.ts.map +1 -1
- package/dist/lifecycle-spool.js +6 -5
- package/dist/lifecycle-spool.js.map +1 -1
- package/dist/normalized-action.d.ts +120 -0
- package/dist/normalized-action.d.ts.map +1 -0
- package/dist/normalized-action.js +1109 -0
- package/dist/normalized-action.js.map +1 -0
- package/dist/runtime-contract.d.ts +108 -4
- package/dist/runtime-contract.d.ts.map +1 -1
- package/dist/runtime-contract.js +178 -35
- package/dist/runtime-contract.js.map +1 -1
- package/dist/types.d.ts +128 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +19 -25
- package/server.json +15 -4
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MARROW_AUTO_RESPONSE_BUDGET_MAX_MS = void 0;
|
|
3
|
+
exports.AGENT_APPROVAL_SETTINGS_PATH = exports.HELD_ACTIONS_PATH = exports.ELICITATION_HOOK_EVENT = exports.MARROW_AUTO_RESPONSE_BUDGET_MAX_MS = void 0;
|
|
4
4
|
exports.writeReconciliationDelayMs = writeReconciliationDelayMs;
|
|
5
5
|
exports.validatePathParam = validatePathParam;
|
|
6
6
|
exports.validateBaseUrl = validateBaseUrl;
|
|
@@ -12,9 +12,13 @@ exports.marrowRevokeKey = marrowRevokeKey;
|
|
|
12
12
|
exports.marrowRotateKey = marrowRotateKey;
|
|
13
13
|
exports.marrowGetKeyAudit = marrowGetKeyAudit;
|
|
14
14
|
exports.marrowThink = marrowThink;
|
|
15
|
+
exports.withoutOwnerApprovalClaim = withoutOwnerApprovalClaim;
|
|
15
16
|
exports.marrowCommit = marrowCommit;
|
|
16
17
|
exports.marrowModelUsage = marrowModelUsage;
|
|
18
|
+
exports.approvalStatement = approvalStatement;
|
|
19
|
+
exports.ordinaryHoldWaitText = ordinaryHoldWaitText;
|
|
17
20
|
exports.marrowAuto = marrowAuto;
|
|
21
|
+
exports.withoutServerNextActions = withoutServerNextActions;
|
|
18
22
|
exports.marrowAgentPatterns = marrowAgentPatterns;
|
|
19
23
|
exports.marrowOrient = marrowOrient;
|
|
20
24
|
exports.marrowAsk = marrowAsk;
|
|
@@ -29,6 +33,11 @@ exports.marrowValueReport = marrowValueReport;
|
|
|
29
33
|
exports.marrowDecisionBrief = marrowDecisionBrief;
|
|
30
34
|
exports.marrowWorkflowGate = marrowWorkflowGate;
|
|
31
35
|
exports.marrowAgentRuntime = marrowAgentRuntime;
|
|
36
|
+
exports.marrowOwnerApprovalStatus = marrowOwnerApprovalStatus;
|
|
37
|
+
exports.marrowRequestApprovalLink = marrowRequestApprovalLink;
|
|
38
|
+
exports.marrowHeldActions = marrowHeldActions;
|
|
39
|
+
exports.marrowAgentApprovalSettings = marrowAgentApprovalSettings;
|
|
40
|
+
exports.marrowHostApproval = marrowHostApproval;
|
|
32
41
|
exports.marrowEnforcement = marrowEnforcement;
|
|
33
42
|
exports.marrowArbitrate = marrowArbitrate;
|
|
34
43
|
exports.marrowGovernanceControlPlane = marrowGovernanceControlPlane;
|
|
@@ -913,10 +922,25 @@ async function marrowThink(apiKey, baseUrl, params, sessionId, agentId, signal,
|
|
|
913
922
|
/**
|
|
914
923
|
* Explicitly commit the result of an action to Marrow.
|
|
915
924
|
*/
|
|
925
|
+
/**
|
|
926
|
+
* A caller-written proof.owner_approval is not an approval: the server issues
|
|
927
|
+
* approvals (host prompt or dashboard) and binds them to the gate receipt. The
|
|
928
|
+
* client never sends such a claim, so a model cannot approve its own hold.
|
|
929
|
+
*/
|
|
930
|
+
function withoutOwnerApprovalClaim(proof) {
|
|
931
|
+
if (!proof || typeof proof !== 'object' || Array.isArray(proof))
|
|
932
|
+
return undefined;
|
|
933
|
+
if (!Object.prototype.hasOwnProperty.call(proof, 'owner_approval'))
|
|
934
|
+
return proof;
|
|
935
|
+
const { owner_approval: _claim, ...rest } = proof;
|
|
936
|
+
return Object.keys(rest).length > 0 ? rest : undefined;
|
|
937
|
+
}
|
|
916
938
|
async function marrowCommit(apiKey, baseUrl, params, sessionId, agentId, signal, idempotencyKey) {
|
|
917
939
|
let runtimeGate = null;
|
|
918
940
|
let gateReceiptId = params.gate_receipt_id || params.gate_receipt;
|
|
919
941
|
let observationOnly = false;
|
|
942
|
+
// An approval is issued by the server (host prompt or dashboard), never written by the caller.
|
|
943
|
+
const commitProof = withoutOwnerApprovalClaim(params.proof);
|
|
920
944
|
for (const [field, value] of [
|
|
921
945
|
['gate receipt', gateReceiptId],
|
|
922
946
|
['arbitration receipt', params.arbitration_receipt_id],
|
|
@@ -936,7 +960,7 @@ async function marrowCommit(apiKey, baseUrl, params, sessionId, agentId, signal,
|
|
|
936
960
|
target: params.target ? (0, redact_1.redactSensitiveText)(params.target) : undefined,
|
|
937
961
|
surfaces: params.surfaces || [],
|
|
938
962
|
context: { mcp_commit_auto_gate: true },
|
|
939
|
-
proof:
|
|
963
|
+
proof: commitProof ? (0, redact_1.redactSensitiveValue)(commitProof) : undefined,
|
|
940
964
|
}, sessionId, agentId, signal);
|
|
941
965
|
}
|
|
942
966
|
catch (err) {
|
|
@@ -970,8 +994,8 @@ async function marrowCommit(apiKey, baseUrl, params, sessionId, agentId, signal,
|
|
|
970
994
|
outcome: (0, redact_1.redactSensitiveText)(params.outcome),
|
|
971
995
|
caused_by: params.caused_by ? (0, redact_1.redactSensitiveText)(params.caused_by) : undefined,
|
|
972
996
|
};
|
|
973
|
-
if (
|
|
974
|
-
body.proof = (0, redact_1.redactSensitiveValue)(
|
|
997
|
+
if (commitProof)
|
|
998
|
+
body.proof = (0, redact_1.redactSensitiveValue)(commitProof);
|
|
975
999
|
if (gateReceiptId)
|
|
976
1000
|
body.gate_receipt_id = gateReceiptId;
|
|
977
1001
|
if (params.arbitration_receipt_id)
|
|
@@ -1175,6 +1199,14 @@ async function waitForAutoContinuation(pending, startedAt, responseBudgetMs, aut
|
|
|
1175
1199
|
}
|
|
1176
1200
|
return responseBudgetMs - (Date.now() - startedAt) > AUTO_RESPONSE_DEADLINE_MARGIN_MS;
|
|
1177
1201
|
}
|
|
1202
|
+
/** Link refusals that another request cannot change. */
|
|
1203
|
+
const FINAL_OWNER_LINK_CODES = new Set([
|
|
1204
|
+
'MARROW_APPROVAL_CHANNEL_UNAVAILABLE', 'MARROW_APPROVAL_LINK_LIMITED', 'MARROW_APPROVAL_LINK_NOT_HELD',
|
|
1205
|
+
'MARROW_APPROVAL_LINK_SCOPE_MISMATCH', 'MARROW_APPROVAL_LINK_INVALID', 'MARROW_APPROVAL_LINK_DECISION_REQUIRED',
|
|
1206
|
+
'MARROW_PRE_ACTION_GATE_USED', 'MARROW_PRE_ACTION_GATE_EXPIRED', 'MARROW_OWNER_APPROVAL_ALREADY_DECIDED',
|
|
1207
|
+
'MARROW_GATE_RECEIPT_NOT_FOUND',
|
|
1208
|
+
]);
|
|
1209
|
+
const MAX_OWNER_LINK_ATTEMPTS = 3;
|
|
1178
1210
|
function autoPartial(input) {
|
|
1179
1211
|
const resumable = input.resumable !== false;
|
|
1180
1212
|
return {
|
|
@@ -1191,6 +1223,7 @@ function autoPartial(input) {
|
|
|
1191
1223
|
exact_next_action: 'Resume marrow_auto with this same operation_id, tenant, agent, session, action, surfaces, outcome, proof, and approval receipts after retry_after_ms. Pending is not confirmation of closure.',
|
|
1192
1224
|
} : {}),
|
|
1193
1225
|
...(input.runtimeGate ? { runtime_gate: input.runtimeGate } : {}),
|
|
1226
|
+
...(input.approval ? { approval: input.approval } : {}),
|
|
1194
1227
|
phase_timings_ms: {
|
|
1195
1228
|
...input.timings,
|
|
1196
1229
|
total: Date.now() - input.startedAt,
|
|
@@ -1198,6 +1231,141 @@ function autoPartial(input) {
|
|
|
1198
1231
|
http_attempt_trace: snapshotAutoHttpTrace(input.autoHttpTrace),
|
|
1199
1232
|
};
|
|
1200
1233
|
}
|
|
1234
|
+
function autoApprovalState(guidance, status, state) {
|
|
1235
|
+
return {
|
|
1236
|
+
state: state || status?.state || 'unavailable',
|
|
1237
|
+
gate_receipt_id: guidance.gateReceiptId,
|
|
1238
|
+
approver: guidance.approvalAuthority,
|
|
1239
|
+
verified_approval_required: guidance.verifiedApprovalRequired,
|
|
1240
|
+
verified_approval_categories: guidance.verifiedApprovalCategories,
|
|
1241
|
+
approval_source: status?.approval_source ?? null,
|
|
1242
|
+
answered_by: status?.approval_answered_by ?? null,
|
|
1243
|
+
poll_after_ms: status?.poll_after_ms ?? guidance.pollAfterMs,
|
|
1244
|
+
expires_at: status?.expires_at ?? guidance.expiresAt,
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* What the status says about an approval of this receipt. "The account owner"
|
|
1249
|
+
* only for the owner's verified approval; anything else is named for what it is.
|
|
1250
|
+
*/
|
|
1251
|
+
function approvalStatement(status, gateReceiptId) {
|
|
1252
|
+
if (status.approval_source === 'host_prompt') {
|
|
1253
|
+
return status.approval_answered_by === 'host_operator' ? `The operator approved gate receipt ${gateReceiptId} in the host prompt (client-attested).`
|
|
1254
|
+
: status.approval_answered_by === 'owner_chat_preapproval' ? `The account owner's chat pre-approval covers gate receipt ${gateReceiptId} (client-attested).`
|
|
1255
|
+
: `An allow rule in the host approved gate receipt ${gateReceiptId} (client-attested).`;
|
|
1256
|
+
}
|
|
1257
|
+
if (status.approval_trust === 'verified'
|
|
1258
|
+
&& (status.approval_answered_by === 'account_owner' || status.approval_source === 'dashboard' || status.approval_source === 'one_tap')) {
|
|
1259
|
+
return `The account owner approved gate receipt ${gateReceiptId}.`;
|
|
1260
|
+
}
|
|
1261
|
+
return `Marrow recorded an approval for gate receipt ${gateReceiptId}.`;
|
|
1262
|
+
}
|
|
1263
|
+
/** What happened to the owner's one-tap link in this operation. Never claims a send that did not happen. */
|
|
1264
|
+
function ownerLinkSentence(guidance, link) {
|
|
1265
|
+
if (!guidance.hostApprovalSupported) {
|
|
1266
|
+
return 'This Marrow service does not support chat or terminal approvals yet, so only the account owner can approve it.';
|
|
1267
|
+
}
|
|
1268
|
+
if (link?.sent)
|
|
1269
|
+
return `An approval link was sent to the account owner${link.channel ? ` (${link.channel})` : ''}.`;
|
|
1270
|
+
if (link?.final) {
|
|
1271
|
+
return `Marrow could not send the account owner an approval link${link.finalCode ? ` (${link.finalCode})` : ''}; tell the operator this action is waiting for the account owner's approval.`;
|
|
1272
|
+
}
|
|
1273
|
+
if (link?.notSent)
|
|
1274
|
+
return 'Nothing was sent to anyone; it waits quietly until a person approves it.';
|
|
1275
|
+
if (link && link.attempts > 0) {
|
|
1276
|
+
return 'Marrow could not send the account owner an approval link yet; calling marrow_auto again with this same operation_id tries again.';
|
|
1277
|
+
}
|
|
1278
|
+
// Owner rule: an ordinary hold sends no email. It waits quietly.
|
|
1279
|
+
return 'Nobody can approve it from this tool call, so it waits quietly; nothing was sent to anyone. Carry on with other work; a person approves it at their next interactive session, where the host asks them.';
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* The text an agent follows while auto waits on a held action. It never asks
|
|
1283
|
+
* the agent to write an approval: only the account owner (one-tap link) or
|
|
1284
|
+
* the operator's host prompt can approve, and the server records it.
|
|
1285
|
+
*/
|
|
1286
|
+
function ordinaryHoldWaitText(guidance, link) {
|
|
1287
|
+
if (guidance.hostApprovalRefusal === 'owner_decline_stands' && !link?.sent) {
|
|
1288
|
+
// The owner said no: the owner is asked again only when the operator asks.
|
|
1289
|
+
const ask = guidance.approvalLinkPath
|
|
1290
|
+
? link && link.attempts > 0
|
|
1291
|
+
? ` ${ownerLinkSentence(guidance, link)}`
|
|
1292
|
+
: ' If the operator asks you to ask the owner again, call marrow_auto again with this same operation_id and request_owner_link: true; Marrow then sends the owner a one-tap approval link.'
|
|
1293
|
+
: '';
|
|
1294
|
+
return `Marrow is holding this action (gate receipt ${guidance.gateReceiptId}). Do not run it. The account owner declined this action${guidance.ownerDeclinedAt ? ` at ${guidance.ownerDeclinedAt}` : ' earlier'}, and only the owner can reverse that.${ask} Never write or claim an approval yourself.`;
|
|
1295
|
+
}
|
|
1296
|
+
const why = guidance.verifiedApprovalRequired === true
|
|
1297
|
+
? ` The account owner approves ${guidance.verifiedApprovalCategories.join(', ') || 'these'} actions personally.`
|
|
1298
|
+
: guidance.hostApprovalRefusal === 'owner_decline_stands'
|
|
1299
|
+
? ` The account owner declined this action${guidance.ownerDeclinedAt ? ` at ${guidance.ownerDeclinedAt}` : ' earlier'}.`
|
|
1300
|
+
: '';
|
|
1301
|
+
return `Marrow is holding this action for approval (gate receipt ${guidance.gateReceiptId}). Do not run it yet.${why} ${ownerLinkSentence(guidance, link)} Call marrow_auto again with this same operation_id after retry_after_ms; Marrow resumes on the same gate receipt once it is approved. Never write or claim an approval yourself.`;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Asks Marrow to send the account owner a one-tap link once per operation; a
|
|
1305
|
+
* retryable failure is tried again on a later call, within the server's limit.
|
|
1306
|
+
*/
|
|
1307
|
+
async function requestAutoOwnerLink(input) {
|
|
1308
|
+
const previous = input.binding.ownerLink;
|
|
1309
|
+
// Nothing was sent because nobody had asked: the operator asking now is a new request.
|
|
1310
|
+
const askedAfterQuiet = previous?.notSent === true && input.personPresent === true;
|
|
1311
|
+
if (previous?.sent || (previous?.final && !askedAfterQuiet) || (previous?.attempts ?? 0) >= MAX_OWNER_LINK_ATTEMPTS)
|
|
1312
|
+
return;
|
|
1313
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
1314
|
+
const timeout = createTimeoutSignal(input.timeoutMs);
|
|
1315
|
+
try {
|
|
1316
|
+
const link = await marrowRequestApprovalLink(input.apiKey, input.baseUrl, input.gateReceiptId, input.decisionId, input.sessionId, input.agentId, timeout.signal, typeof input.personPresent === 'boolean' ? { personPresent: input.personPresent } : {});
|
|
1317
|
+
if (link.ok) {
|
|
1318
|
+
input.binding.ownerLink = { sent: true, ...(link.alreadySent ? { alreadySent: true } : {}), expiresAt: link.link.expires_at ?? null, channel: link.link.channel, attempts, finalCode: null, final: false };
|
|
1319
|
+
}
|
|
1320
|
+
else if (link.notSent) {
|
|
1321
|
+
input.binding.ownerLink = { sent: false, notSent: true, channel: null, attempts, finalCode: link.code, final: true };
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
const final = (!link.retryable && Boolean(link.code && FINAL_OWNER_LINK_CODES.has(link.code))) || attempts >= MAX_OWNER_LINK_ATTEMPTS;
|
|
1325
|
+
input.binding.ownerLink = { sent: false, channel: null, attempts, finalCode: link.code, final };
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
catch {
|
|
1329
|
+
input.binding.ownerLink = { sent: false, channel: null, attempts, finalCode: null, final: attempts >= MAX_OWNER_LINK_ATTEMPTS };
|
|
1330
|
+
}
|
|
1331
|
+
finally {
|
|
1332
|
+
timeout.cancel();
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
async function readOrdinaryApprovalForAuto(input) {
|
|
1336
|
+
let status = null;
|
|
1337
|
+
const maxReads = Math.max(1, Math.min(3, input.maxReads ?? 3));
|
|
1338
|
+
// Poll as the server advises, inside auto's bounded response budget.
|
|
1339
|
+
for (let reads = 0; reads < maxReads; reads += 1) {
|
|
1340
|
+
const remaining = input.responseBudgetMs - (Date.now() - input.startedAt) - AUTO_RESPONSE_DEADLINE_MARGIN_MS;
|
|
1341
|
+
if (remaining < 250)
|
|
1342
|
+
break;
|
|
1343
|
+
const timeout = createTimeoutSignal(input.responseBudgetMs, input.startedAt);
|
|
1344
|
+
try {
|
|
1345
|
+
const result = await marrowOwnerApprovalStatus(input.apiKey, input.baseUrl, input.guidance.gateReceiptId, input.sessionId, input.agentId, timeout.signal);
|
|
1346
|
+
if (result.kind === 'not_found')
|
|
1347
|
+
return { status: null, notFound: true };
|
|
1348
|
+
status = result.status;
|
|
1349
|
+
}
|
|
1350
|
+
catch (error) {
|
|
1351
|
+
const failure = (0, request_reliability_1.normalizeRequestError)(error);
|
|
1352
|
+
if (failure.retryable || failure.code === 'request_timeout')
|
|
1353
|
+
break;
|
|
1354
|
+
throw error;
|
|
1355
|
+
}
|
|
1356
|
+
finally {
|
|
1357
|
+
timeout.cancel();
|
|
1358
|
+
}
|
|
1359
|
+
if (!status || (status.state !== 'pending' && status.state !== 'unavailable' && status.state !== 'arbitration_review'))
|
|
1360
|
+
break;
|
|
1361
|
+
if (reads + 1 >= maxReads)
|
|
1362
|
+
break;
|
|
1363
|
+
const waitMs = status.poll_after_ms ?? input.guidance.pollAfterMs;
|
|
1364
|
+
if (!await waitForAutoContinuation({ retry_after_ms: waitMs }, input.startedAt, input.responseBudgetMs, input.autoHttpTrace))
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
return { status, notFound: false };
|
|
1368
|
+
}
|
|
1201
1369
|
/**
|
|
1202
1370
|
* Bounded outcome logging helper for tool hooks and simple integrations.
|
|
1203
1371
|
* One outer invocation logs intent and, when an outcome is supplied, continues
|
|
@@ -1205,16 +1373,78 @@ function autoPartial(input) {
|
|
|
1205
1373
|
* caller's deadline is reached, the same operation ID resumes without opening
|
|
1206
1374
|
* another decision.
|
|
1207
1375
|
*/
|
|
1208
|
-
async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs) {
|
|
1376
|
+
async function marrowAuto(apiKey, baseUrl, params, sessionId, agentId, timeoutMs, options) {
|
|
1209
1377
|
const autoHttpTrace = { attempts: [], dropped_count: 0 };
|
|
1210
1378
|
try {
|
|
1211
|
-
|
|
1379
|
+
const result = await marrowAutoWithTrace(apiKey, baseUrl, params, sessionId, agentId, timeoutMs, autoHttpTrace, options);
|
|
1380
|
+
// The runtime's next-step texts are written for API clients (endpoints to
|
|
1381
|
+
// call); the raw runtime_gate an agent reads never carries them.
|
|
1382
|
+
return result.runtime_gate ? { ...result, runtime_gate: withoutServerNextActions(result.runtime_gate) } : result;
|
|
1212
1383
|
}
|
|
1213
1384
|
catch (error) {
|
|
1214
1385
|
throw attachAutoHttpTrace(error, autoHttpTrace);
|
|
1215
1386
|
}
|
|
1216
1387
|
}
|
|
1217
|
-
|
|
1388
|
+
/** Removes every exact_next_action(s) from a server response (bounded depth). */
|
|
1389
|
+
function withoutServerNextActions(value, depth = 0) {
|
|
1390
|
+
if (depth > 16 || !value || typeof value !== 'object')
|
|
1391
|
+
return value;
|
|
1392
|
+
if (Array.isArray(value))
|
|
1393
|
+
return value.map((item) => withoutServerNextActions(item, depth + 1));
|
|
1394
|
+
const out = {};
|
|
1395
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1396
|
+
if (key === 'exact_next_action' || key === 'exact_next_actions')
|
|
1397
|
+
continue;
|
|
1398
|
+
out[key] = withoutServerNextActions(item, depth + 1);
|
|
1399
|
+
}
|
|
1400
|
+
return out;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* The operator marker the backend needs to count an elicitation answer as the
|
|
1404
|
+
* operator's. Client-attested: Marrow trusts the MCP client to show the dialog
|
|
1405
|
+
* to a person (the agent cannot answer it through any tool argument). A client
|
|
1406
|
+
* that let the model answer elicitation requests would make the agent's own
|
|
1407
|
+
* answer count as the operator's.
|
|
1408
|
+
*/
|
|
1409
|
+
exports.ELICITATION_HOOK_EVENT = 'mcp_elicitation';
|
|
1410
|
+
async function elicitAutoApproval(input) {
|
|
1411
|
+
const askedAt = new Date().toISOString();
|
|
1412
|
+
const notice = input.guidance.operatorNotice ? ` Note: ${input.guidance.operatorNotice}` : '';
|
|
1413
|
+
let answer = null;
|
|
1414
|
+
try {
|
|
1415
|
+
answer = await input.elicit({
|
|
1416
|
+
gateReceiptId: input.guidance.gateReceiptId,
|
|
1417
|
+
message: `Marrow holds this action for your approval: ${input.action}.${input.reason ? ` Reason: ${input.reason}` : ''}${notice} Approve only if you authorize this exact action.`.slice(0, 900),
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
catch {
|
|
1421
|
+
answer = null;
|
|
1422
|
+
}
|
|
1423
|
+
if (!answer)
|
|
1424
|
+
return null;
|
|
1425
|
+
const report = {
|
|
1426
|
+
verdict: answer,
|
|
1427
|
+
host: input.host || 'other',
|
|
1428
|
+
host_session_id: input.hostSessionId || input.sessionId || 'mcp-session',
|
|
1429
|
+
hook_event: exports.ELICITATION_HOOK_EVENT,
|
|
1430
|
+
pre_action_event_id: null,
|
|
1431
|
+
asked_at: askedAt,
|
|
1432
|
+
answered_at: new Date().toISOString(),
|
|
1433
|
+
...(input.decisionId ? { decision_id: input.decisionId } : {}),
|
|
1434
|
+
};
|
|
1435
|
+
const timeout = createTimeoutSignal(4_000);
|
|
1436
|
+
try {
|
|
1437
|
+
const result = await marrowHostApproval(input.apiKey, input.baseUrl, input.guidance.gateReceiptId, report, input.sessionId, input.agentId, timeout.signal);
|
|
1438
|
+
return result.ok ? answer : null;
|
|
1439
|
+
}
|
|
1440
|
+
catch {
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
finally {
|
|
1444
|
+
timeout.cancel();
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
async function marrowAutoWithTrace(apiKey, baseUrl, params, sessionId, agentId, timeoutMs, autoHttpTrace, options) {
|
|
1218
1448
|
const startedAt = Date.now();
|
|
1219
1449
|
const responseBudgetMs = autoResponseBudget(timeoutMs);
|
|
1220
1450
|
const operationId = resolveAutoOperationId(params.operation_id);
|
|
@@ -1286,7 +1516,8 @@ async function marrowAutoWithTrace(apiKey, baseUrl, params, sessionId, agentId,
|
|
|
1286
1516
|
}
|
|
1287
1517
|
const thinkStarted = Date.now();
|
|
1288
1518
|
let decisionId = operationBinding.decisionId || null;
|
|
1289
|
-
const
|
|
1519
|
+
const ordinaryGuidance = runtimeGate ? (0, runtime_contract_1.ordinaryApprovalGuidance)(runtimeGate) : null;
|
|
1520
|
+
const ordinaryApprovalDeclared = Boolean(ordinaryGuidance);
|
|
1290
1521
|
if (runtimeGate && !runtimeGate.arbitration && (runtimeGate.decision_id || ordinaryApprovalDeclared)) {
|
|
1291
1522
|
if (!(0, runtime_contract_1.runtimeDecisionMatchesAutoScope)(runtimeGate, {
|
|
1292
1523
|
action: (0, redact_1.redactSensitiveText)(params.action_for_gate || params.action), agentId, sessionId,
|
|
@@ -1319,7 +1550,7 @@ async function marrowAutoWithTrace(apiKey, baseUrl, params, sessionId, agentId,
|
|
|
1319
1550
|
message: 'marrow_auto operation is already bound to a different decision than the runtime arbitration receipt.',
|
|
1320
1551
|
status: 409,
|
|
1321
1552
|
retryable: false,
|
|
1322
|
-
exactFix: 'Stop this operation. Start a new arbitrated marrow_auto operation and preserve the arbitration decision_id, gate receipt, arbitration receipt, and
|
|
1553
|
+
exactFix: 'Stop this operation. Start a new arbitrated marrow_auto operation and preserve the arbitration decision_id, gate receipt, arbitration receipt, and owner approval receipt together.',
|
|
1323
1554
|
});
|
|
1324
1555
|
}
|
|
1325
1556
|
decisionId = arbitrationDecisionId;
|
|
@@ -1392,18 +1623,6 @@ async function marrowAutoWithTrace(apiKey, baseUrl, params, sessionId, agentId,
|
|
|
1392
1623
|
}
|
|
1393
1624
|
}
|
|
1394
1625
|
timings.think = reusedDecision ? 0 : Date.now() - thinkStarted;
|
|
1395
|
-
if (params.outcome === undefined || typeof params.success !== 'boolean') {
|
|
1396
|
-
return autoPartial({
|
|
1397
|
-
operationId,
|
|
1398
|
-
decisionId,
|
|
1399
|
-
phase: 'decision_created',
|
|
1400
|
-
runtimeGate,
|
|
1401
|
-
timings,
|
|
1402
|
-
startedAt,
|
|
1403
|
-
autoHttpTrace,
|
|
1404
|
-
retryAfterMs: null,
|
|
1405
|
-
});
|
|
1406
|
-
}
|
|
1407
1626
|
const runtimeReviewRequired = Boolean(runtimeGate && (runtimeGate.risk_gate?.decision === 'review_required'
|
|
1408
1627
|
|| runtimeGate.gate_receipt?.decision === 'review_required'
|
|
1409
1628
|
|| runtimeGate.gate_receipt?.decision === 'owner_approval_required'
|
|
@@ -1418,23 +1637,226 @@ async function marrowAutoWithTrace(apiKey, baseUrl, params, sessionId, agentId,
|
|
|
1418
1637
|
|| runtimeGate.arbitration.owner_approval_required === true));
|
|
1419
1638
|
const genericReviewRequired = runtimeReviewRequired && !runtimeGate?.arbitration;
|
|
1420
1639
|
const runtimeArbitrationReceiptId = runtimeGate?.arbitration?.receipt_id;
|
|
1421
|
-
|
|
1640
|
+
// Arbitration review with the server's one-tap path: the owner picks a
|
|
1641
|
+
// proposal through a link, and the status read hands over the owner's receipt.
|
|
1642
|
+
let ownerApprovalReceiptForCommit = params.owner_approval_receipt_id;
|
|
1643
|
+
let arbitrationReceiptForCommit = params.arbitration_receipt_id;
|
|
1644
|
+
const arbitrationGuidance = arbitrationRequiresOwnerApproval && !params.owner_approval_receipt_id
|
|
1645
|
+
? (0, runtime_contract_1.arbitrationApprovalGuidance)(runtimeGate)
|
|
1646
|
+
: null;
|
|
1647
|
+
if (arbitrationGuidance) {
|
|
1648
|
+
const arbitrationWait = (exactNextAction, retryAfterMs, resumable = true) => autoPartial({
|
|
1649
|
+
operationId, decisionId, phase: 'owner_approval_required', runtimeGate, timings, startedAt, autoHttpTrace,
|
|
1650
|
+
resumable, retryAfterMs: retryAfterMs ?? undefined, exactNextAction,
|
|
1651
|
+
});
|
|
1652
|
+
const linkBudget = responseBudgetMs - (Date.now() - startedAt) - AUTO_RESPONSE_DEADLINE_MARGIN_MS - 300;
|
|
1653
|
+
if (linkBudget > 300) {
|
|
1654
|
+
await requestAutoOwnerLink({
|
|
1655
|
+
apiKey, baseUrl, gateReceiptId: arbitrationGuidance.gateReceiptId, decisionId, sessionId, agentId,
|
|
1656
|
+
binding: operationBinding, timeoutMs: Math.min(2_000, linkBudget),
|
|
1657
|
+
// Client-attested: a person at this session asked only when the operator asked for the link.
|
|
1658
|
+
personPresent: params.request_owner_link === true,
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
const read = await readOrdinaryApprovalForAuto({
|
|
1662
|
+
apiKey, baseUrl, guidance: arbitrationGuidance, sessionId, agentId, startedAt, responseBudgetMs, autoHttpTrace,
|
|
1663
|
+
});
|
|
1664
|
+
const status = read.status;
|
|
1665
|
+
const link = operationBinding.ownerLink;
|
|
1666
|
+
const linkText = link?.alreadySent ? `The account owner already has a one-tap approval link${link.channel ? ` (${link.channel}${link.expiresAt ? `, until ${link.expiresAt}` : ''})` : ''}; wait for their answer.`
|
|
1667
|
+
: link?.sent ? `An approval link was sent to the account owner${link.channel ? ` (${link.channel})` : ''}.`
|
|
1668
|
+
: link?.notSent ? 'Nothing was sent to the account owner. If the operator wants the owner asked now, call marrow_auto again with this same operation_id and request_owner_link: true; Marrow then sends the owner a one-tap link.'
|
|
1669
|
+
: link?.final ? `Marrow could not send the account owner an approval link${link.finalCode ? ` (${link.finalCode})` : ''}; tell the operator this action is waiting for the account owner's choice.`
|
|
1670
|
+
: link && link.attempts > 0 ? 'Marrow could not send the account owner an approval link yet; calling marrow_auto again with this same operation_id tries again.'
|
|
1671
|
+
: 'Tell the operator this action is waiting for the account owner\'s choice.';
|
|
1672
|
+
if (read.notFound) {
|
|
1673
|
+
return arbitrationWait(`Marrow could not find gate receipt ${arbitrationGuidance.gateReceiptId} for this agent and session. Do not run any proposal; request fresh runtime guidance for it.`, null, false);
|
|
1674
|
+
}
|
|
1675
|
+
if (!status || status.state === 'arbitration_review' || status.state === 'pending' || status.state === 'unavailable') {
|
|
1676
|
+
return arbitrationWait(`Marrow is holding this action for arbitration review (gate receipt ${arbitrationGuidance.gateReceiptId}). Do not run any proposal yet. The account owner picks and approves one proposal. ${linkText} Call marrow_auto again with this same operation_id after retry_after_ms. Never write or claim an approval yourself.`, status?.poll_after_ms ?? arbitrationGuidance.pollAfterMs);
|
|
1677
|
+
}
|
|
1678
|
+
if (status.state === 'approved' && status.owner_approval_receipt_id) {
|
|
1679
|
+
ownerApprovalReceiptForCommit = status.owner_approval_receipt_id;
|
|
1680
|
+
arbitrationReceiptForCommit = arbitrationGuidance.arbitrationReceiptId;
|
|
1681
|
+
}
|
|
1682
|
+
else if (status.state === 'used') {
|
|
1683
|
+
return {
|
|
1684
|
+
operation_id: operationId, decision_id: decisionId, committed: false, phase: 'closed', resumable: false, retry_after_ms: null,
|
|
1685
|
+
exact_next_action: `Gate receipt ${arbitrationGuidance.gateReceiptId} is already used: this operation is closed. Do not run this action again.`,
|
|
1686
|
+
runtime_gate: runtimeGate, closure: status.owner_approval_receipt_id ? 'already_closed' : 'already_closed_denial',
|
|
1687
|
+
phase_timings_ms: { ...timings, total: Date.now() - startedAt }, http_attempt_trace: snapshotAutoHttpTrace(autoHttpTrace),
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
else if (status.state === 'declined') {
|
|
1691
|
+
return arbitrationWait(`The account owner approved none of the proposals (gate receipt ${arbitrationGuidance.gateReceiptId}). Do not run any proposal. If nothing ran, close it with marrow_commit: success false, an outcome that starts "Denied by Marrow pre-action gate", and the same gate_receipt_id.`, null, false);
|
|
1692
|
+
}
|
|
1693
|
+
else {
|
|
1694
|
+
return arbitrationWait(`Gate receipt ${arbitrationGuidance.gateReceiptId} is not waiting for the owner's choice (${status.state}). Do not run any proposal; request fresh runtime guidance for this exact action.`, null, false);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
const matchingRequiredApprovalReceipts = Boolean(ownerApprovalReceiptForCommit
|
|
1422
1698
|
&& (!runtimeArbitrationReceiptId
|
|
1423
|
-
||
|
|
1424
|
-
|
|
1425
|
-
|
|
1699
|
+
|| arbitrationReceiptForCommit === runtimeArbitrationReceiptId));
|
|
1700
|
+
// An ordinary hold: wait for the approval through the agent-key status read
|
|
1701
|
+
// (bounded by this call's budget), then resume on the same gate receipt.
|
|
1702
|
+
// Approval comes only from the server (the account owner's one-tap link, or
|
|
1703
|
+
// the operator's host prompt recorded by the host hook), never from proof.
|
|
1704
|
+
let ordinaryApproval = null;
|
|
1705
|
+
let ordinaryApprovalState;
|
|
1706
|
+
if (genericReviewRequired && ordinaryGuidance && runtimeGate) {
|
|
1707
|
+
// In flow first: where the MCP client can ask its user (elicitation), the
|
|
1708
|
+
// person answers right here, once per held action; the answer is reported
|
|
1709
|
+
// through the host-approval route (client-attested), never by the agent.
|
|
1710
|
+
let elicited = null;
|
|
1711
|
+
if (ordinaryGuidance.hostApprovalAccepted && options?.elicitApproval && !operationBinding.elicitedReceipts?.includes(ordinaryGuidance.gateReceiptId)) {
|
|
1712
|
+
operationBinding.elicitedReceipts = [...(operationBinding.elicitedReceipts || []), ordinaryGuidance.gateReceiptId].slice(-8);
|
|
1713
|
+
elicited = await elicitAutoApproval({
|
|
1714
|
+
apiKey, baseUrl, guidance: ordinaryGuidance, decisionId, sessionId, agentId,
|
|
1715
|
+
action: (0, redact_1.redactSensitiveText)(params.action_for_gate || params.action).slice(0, 300),
|
|
1716
|
+
reason: typeof runtimeGate.risk_gate?.reasons?.[0]?.message === 'string' ? runtimeGate.risk_gate.reasons[0].message.slice(0, 240) : '',
|
|
1717
|
+
elicit: options.elicitApproval,
|
|
1718
|
+
host: options.elicitHost,
|
|
1719
|
+
hostSessionId: options.elicitHostSessionId,
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
// Owner rule: the owner's link only for an owner-locked category, the owner's
|
|
1723
|
+
// own standing decline when the operator asks, or (with no way to ask anyone
|
|
1724
|
+
// here) an unattended run whose owner turned on pings. Otherwise quiet.
|
|
1725
|
+
const policy = (0, runtime_contract_1.ownerLinkPolicy)(ordinaryGuidance, { unattended: !options?.elicitApproval });
|
|
1726
|
+
const linkWanted = elicited === null && (policy === 'now' || (policy === 'on_request' && params.request_owner_link === true));
|
|
1727
|
+
const linkBudget = responseBudgetMs - (Date.now() - startedAt) - AUTO_RESPONSE_DEADLINE_MARGIN_MS - 300;
|
|
1728
|
+
if (ordinaryGuidance.approvalLinkPath && linkWanted && linkBudget > 300) {
|
|
1729
|
+
await requestAutoOwnerLink({
|
|
1730
|
+
apiKey, baseUrl, gateReceiptId: ordinaryGuidance.gateReceiptId, decisionId, sessionId, agentId,
|
|
1731
|
+
binding: operationBinding, timeoutMs: Math.min(2_000, linkBudget),
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
// After a person answered here, read the status once with its own time (the answer took human time).
|
|
1735
|
+
const read = elicited
|
|
1736
|
+
? await readOrdinaryApprovalForAuto({
|
|
1737
|
+
apiKey, baseUrl, guidance: ordinaryGuidance, sessionId, agentId, startedAt: Date.now(), responseBudgetMs, autoHttpTrace,
|
|
1738
|
+
})
|
|
1739
|
+
: await readOrdinaryApprovalForAuto({
|
|
1740
|
+
apiKey, baseUrl, guidance: ordinaryGuidance, sessionId, agentId, startedAt, responseBudgetMs, autoHttpTrace,
|
|
1741
|
+
maxReads: ordinaryGuidance.hostApprovalSupported ? 3 : 1,
|
|
1742
|
+
});
|
|
1743
|
+
const status = read.status;
|
|
1744
|
+
ordinaryApprovalState = autoApprovalState(ordinaryGuidance, status, read.notFound ? 'not_found' : undefined);
|
|
1745
|
+
const terminal = (exactNextAction) => autoPartial({
|
|
1746
|
+
operationId, decisionId, phase: 'review_required', runtimeGate, timings, startedAt, autoHttpTrace,
|
|
1747
|
+
resumable: false, exactNextAction, approval: ordinaryApprovalState,
|
|
1748
|
+
});
|
|
1749
|
+
if (read.notFound) {
|
|
1750
|
+
return terminal(`Marrow could not find gate receipt ${ordinaryGuidance.gateReceiptId} for this agent and session. Do not run the action; request fresh runtime guidance for it.`);
|
|
1751
|
+
}
|
|
1752
|
+
if (!status || status.state === 'pending' || status.state === 'unavailable') {
|
|
1426
1753
|
return autoPartial({
|
|
1427
1754
|
operationId, decisionId, phase: 'owner_approval_required', runtimeGate, timings, startedAt, autoHttpTrace,
|
|
1428
|
-
resumable:
|
|
1429
|
-
|
|
1755
|
+
resumable: true,
|
|
1756
|
+
retryAfterMs: status?.poll_after_ms ?? ordinaryGuidance.pollAfterMs,
|
|
1757
|
+
exactNextAction: ordinaryHoldWaitText(ordinaryGuidance, operationBinding.ownerLink),
|
|
1758
|
+
approval: ordinaryApprovalState,
|
|
1430
1759
|
});
|
|
1431
1760
|
}
|
|
1432
|
-
|
|
1761
|
+
if (status.state === 'declined') {
|
|
1762
|
+
const declinedBy = status.approval_source === 'host_prompt' ? 'The operator declined it in the host prompt' : 'The account owner declined it';
|
|
1763
|
+
if (params.outcome === undefined && decisionId && Date.now() - startedAt < responseBudgetMs - 250) {
|
|
1764
|
+
// The action has not run: close the decision as a verified gate denial.
|
|
1765
|
+
const denialTimeout = createTimeoutSignal(responseBudgetMs, startedAt);
|
|
1766
|
+
try {
|
|
1767
|
+
const denial = await marrowCommit(apiKey, baseUrl, {
|
|
1768
|
+
decision_id: decisionId,
|
|
1769
|
+
success: false,
|
|
1770
|
+
outcome: `Denied by Marrow pre-action gate: ${declinedBy.charAt(0).toLowerCase()}${declinedBy.slice(1)} (gate receipt ${ordinaryGuidance.gateReceiptId}); the action did not run.`,
|
|
1771
|
+
gate_receipt_id: gateReceiptId,
|
|
1772
|
+
auto_gate: false,
|
|
1773
|
+
[AUTO_MANAGED_WRITE]: true,
|
|
1774
|
+
[AUTO_HTTP_TRACE]: autoHttpTrace,
|
|
1775
|
+
}, sessionId, agentId, denialTimeout.signal, autoIdempotencyKey(operationId, 'commit'));
|
|
1776
|
+
if (denial.committed) {
|
|
1777
|
+
return {
|
|
1778
|
+
operation_id: operationId,
|
|
1779
|
+
decision_id: decisionId,
|
|
1780
|
+
committed: true,
|
|
1781
|
+
phase: 'closed',
|
|
1782
|
+
resumable: false,
|
|
1783
|
+
retry_after_ms: null,
|
|
1784
|
+
exact_next_action: `${declinedBy}. Marrow closed this decision as a denial. Do not run this action.`,
|
|
1785
|
+
runtime_gate: runtimeGate,
|
|
1786
|
+
approval: ordinaryApprovalState,
|
|
1787
|
+
closure: 'gate_denial',
|
|
1788
|
+
phase_timings_ms: { ...timings, total: Date.now() - startedAt },
|
|
1789
|
+
http_attempt_trace: snapshotAutoHttpTrace(autoHttpTrace),
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
catch { /* the decline stands; the denial closure stays open below */ }
|
|
1794
|
+
finally {
|
|
1795
|
+
denialTimeout.cancel();
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return terminal(`${declinedBy} (gate receipt ${ordinaryGuidance.gateReceiptId}). Do not run this action. If it has not run, close it with marrow_commit: success false, an outcome that starts "Denied by Marrow pre-action gate", and the same gate_receipt_id.`);
|
|
1799
|
+
}
|
|
1800
|
+
if (status.state === 'expired') {
|
|
1801
|
+
return terminal(`Gate receipt ${ordinaryGuidance.gateReceiptId} expired before it was approved. Do not run the action on it. If the work is still needed and has not run, start a new marrow_auto operation for a fresh gate.`);
|
|
1802
|
+
}
|
|
1803
|
+
if (status.state === 'used') {
|
|
1804
|
+
// A spent receipt is final: never resume from it, and never call it approved now.
|
|
1805
|
+
const approvedBefore = Boolean(status.owner_approval_receipt_id);
|
|
1806
|
+
return {
|
|
1807
|
+
operation_id: operationId,
|
|
1808
|
+
decision_id: decisionId,
|
|
1809
|
+
committed: false,
|
|
1810
|
+
phase: 'closed',
|
|
1811
|
+
resumable: false,
|
|
1812
|
+
retry_after_ms: null,
|
|
1813
|
+
exact_next_action: approvedBefore
|
|
1814
|
+
? `Gate receipt ${ordinaryGuidance.gateReceiptId} was approved and is already used: this operation is closed. Do not run this action again.`
|
|
1815
|
+
: `The approval request for gate receipt ${ordinaryGuidance.gateReceiptId} was declined, and the decision is closed as a denial. Do not run this action.`,
|
|
1816
|
+
runtime_gate: runtimeGate,
|
|
1817
|
+
approval: ordinaryApprovalState,
|
|
1818
|
+
closure: approvedBefore ? 'already_closed' : 'already_closed_denial',
|
|
1819
|
+
phase_timings_ms: { ...timings, total: Date.now() - startedAt },
|
|
1820
|
+
http_attempt_trace: snapshotAutoHttpTrace(autoHttpTrace),
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
if (status.state === 'approved') {
|
|
1824
|
+
ordinaryApproval = status;
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
return terminal(`Gate receipt ${ordinaryGuidance.gateReceiptId} is not waiting for an approval (${status.state}). Do not run the action on it; stop this operation and request fresh runtime guidance for this exact action.`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
if (params.outcome === undefined || typeof params.success !== 'boolean') {
|
|
1831
|
+
return autoPartial({
|
|
1832
|
+
operationId,
|
|
1833
|
+
decisionId,
|
|
1834
|
+
phase: 'decision_created',
|
|
1835
|
+
runtimeGate,
|
|
1836
|
+
timings,
|
|
1837
|
+
startedAt,
|
|
1838
|
+
autoHttpTrace,
|
|
1839
|
+
retryAfterMs: null,
|
|
1840
|
+
...(arbitrationGuidance && ownerApprovalReceiptForCommit ? {
|
|
1841
|
+
exactNextAction: `The account owner approved one proposal for arbitration receipt ${arbitrationGuidance.arbitrationReceiptId} (owner_approval_receipt_id ${ownerApprovalReceiptForCommit}). Run only the approved proposal now, then call marrow_auto with this same operation_id, the real outcome and success${arbitrationGuidance.proofRequired ? `, and proof with ${arbitrationGuidance.proofFields.join(', ') || 'the required fields'}` : ''}. Marrow closes it with that receipt.`,
|
|
1842
|
+
} : {}),
|
|
1843
|
+
...(ordinaryApproval && ordinaryGuidance ? {
|
|
1844
|
+
exactNextAction: `${approvalStatement(ordinaryApproval, ordinaryGuidance.gateReceiptId)} Run only this exact action now, then call marrow_auto with this same operation_id, the real outcome and success${ordinaryGuidance.proofRequired ? `, and proof with ${ordinaryGuidance.proofFields.join(', ') || 'the required fields'}` : ''}. Marrow closes it on the same gate receipt.`,
|
|
1845
|
+
approval: ordinaryApprovalState,
|
|
1846
|
+
} : {}),
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
if (ordinaryApproval && ordinaryGuidance) {
|
|
1850
|
+
// A proof-required hold closes trusted only with its proof; committing
|
|
1851
|
+
// without it would leave an unverified observation that blocks the later
|
|
1852
|
+
// trusted close, so auto waits for the proof instead.
|
|
1853
|
+
const commitProof = withoutOwnerApprovalClaim(params.proof);
|
|
1854
|
+
proofCanClose = !ordinaryGuidance.proofRequired || Boolean(commitProof && Object.keys(commitProof).length > 0);
|
|
1433
1855
|
if (!proofCanClose) {
|
|
1434
1856
|
return autoPartial({
|
|
1435
|
-
operationId, decisionId, phase: '
|
|
1436
|
-
|
|
1437
|
-
exactNextAction:
|
|
1857
|
+
operationId, decisionId, phase: 'proof_required', runtimeGate, timings, startedAt, autoHttpTrace,
|
|
1858
|
+
retryAfterMs: null, approval: ordinaryApprovalState,
|
|
1859
|
+
exactNextAction: `Approved. Attach measured proof with ${ordinaryGuidance.proofFields.join(', ') || 'the required fields'} and call marrow_auto again with this same operation_id; Marrow closes it on gate receipt ${ordinaryGuidance.gateReceiptId}.`,
|
|
1438
1860
|
});
|
|
1439
1861
|
}
|
|
1440
1862
|
}
|
|
@@ -1510,8 +1932,8 @@ async function marrowAutoWithTrace(apiKey, baseUrl, params, sessionId, agentId,
|
|
|
1510
1932
|
outcome: params.outcome,
|
|
1511
1933
|
proof: params.proof,
|
|
1512
1934
|
gate_receipt_id: gateReceiptId,
|
|
1513
|
-
arbitration_receipt_id:
|
|
1514
|
-
owner_approval_receipt_id:
|
|
1935
|
+
arbitration_receipt_id: arbitrationReceiptForCommit,
|
|
1936
|
+
owner_approval_receipt_id: ownerApprovalReceiptForCommit,
|
|
1515
1937
|
action: params.action_for_gate || params.action,
|
|
1516
1938
|
type: params.type || 'general',
|
|
1517
1939
|
surfaces: params.surfaces,
|
|
@@ -1560,6 +1982,7 @@ async function marrowAutoWithTrace(apiKey, baseUrl, params, sessionId, agentId,
|
|
|
1560
1982
|
resumable: false,
|
|
1561
1983
|
retry_after_ms: null,
|
|
1562
1984
|
...(runtimeGate ? { runtime_gate: runtimeGate } : {}),
|
|
1985
|
+
...(ordinaryApprovalState ? { approval: { ...ordinaryApprovalState, state: 'used' } } : {}),
|
|
1563
1986
|
phase_timings_ms: {
|
|
1564
1987
|
...timings,
|
|
1565
1988
|
total: Date.now() - startedAt,
|
|
@@ -1978,6 +2401,298 @@ async function marrowAgentRuntime(apiKey, baseUrl, input, sessionId, agentId, si
|
|
|
1978
2401
|
}
|
|
1979
2402
|
return runtime;
|
|
1980
2403
|
}
|
|
2404
|
+
const GATE_RECEIPT_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,199}$/;
|
|
2405
|
+
const OWNER_APPROVAL_STATES = new Set(['pending', 'approved', 'declined', 'expired', 'used', 'not_held', 'arbitration_review', 'unavailable']);
|
|
2406
|
+
const SAFE_STATUS_TEXT = (value, limit) => typeof value === 'string' && value.trim() ? (0, redact_1.redactSensitiveText)(value.replace(/[\u0000-\u001f\u007f]/g, ' ').trim()).slice(0, limit) : null;
|
|
2407
|
+
const SAFE_STATUS_ID = (value) => typeof value === 'string' && GATE_RECEIPT_IDENTIFIER.test(value) ? value : null;
|
|
2408
|
+
const SAFE_STATUS_TIME = (value) => typeof value === 'string' && value.length <= 40 && Number.isFinite(Date.parse(value)) ? value : null;
|
|
2409
|
+
function normalizeOwnerApprovalStatus(value, gateReceiptId) {
|
|
2410
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
2411
|
+
return null;
|
|
2412
|
+
const view = value;
|
|
2413
|
+
if (view.gate_receipt_id !== gateReceiptId)
|
|
2414
|
+
return null;
|
|
2415
|
+
const state = typeof view.state === 'string' && OWNER_APPROVAL_STATES.has(view.state) ? view.state : 'unavailable';
|
|
2416
|
+
return {
|
|
2417
|
+
gate_receipt_id: gateReceiptId,
|
|
2418
|
+
decision_id: SAFE_STATUS_ID(view.decision_id),
|
|
2419
|
+
state,
|
|
2420
|
+
gate_decision: SAFE_STATUS_TEXT(view.gate_decision, 64),
|
|
2421
|
+
owner_approval_receipt_id: SAFE_STATUS_ID(view.owner_approval_receipt_id),
|
|
2422
|
+
decided_at: SAFE_STATUS_TIME(view.decided_at),
|
|
2423
|
+
approval_source: SAFE_STATUS_TEXT(view.approval_source, 32),
|
|
2424
|
+
approval_trust: SAFE_STATUS_TEXT(view.approval_trust, 32),
|
|
2425
|
+
approval_answered_by: SAFE_STATUS_TEXT(view.approval_answered_by, 32),
|
|
2426
|
+
expires_at: SAFE_STATUS_TIME(view.expires_at),
|
|
2427
|
+
terminal: view.terminal === true,
|
|
2428
|
+
retryable: view.retryable === true,
|
|
2429
|
+
poll_after_ms: typeof view.poll_after_ms === 'number' && Number.isFinite(view.poll_after_ms) ? (0, runtime_contract_1.boundedPollAfterMs)(view.poll_after_ms) : null,
|
|
2430
|
+
exact_next_action: SAFE_STATUS_TEXT(view.exact_next_action, 600) || '',
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* GET /v1/agent/gate-receipts/:id/owner-approval with the agent's own key:
|
|
2435
|
+
* whether a held gate receipt was approved (host prompt or dashboard),
|
|
2436
|
+
* declined, expired or used. An unknown receipt, or one of another agent or
|
|
2437
|
+
* session, answers not_found. Never authorizes anything by itself.
|
|
2438
|
+
*/
|
|
2439
|
+
async function marrowOwnerApprovalStatus(apiKey, baseUrl, gateReceiptId, sessionId, agentId, signal) {
|
|
2440
|
+
if (!GATE_RECEIPT_IDENTIFIER.test(gateReceiptId))
|
|
2441
|
+
throw new TypeError('gate_receipt_id is not a valid gate receipt identifier.');
|
|
2442
|
+
return fetch(`${baseUrl}${(0, runtime_contract_1.ownerApprovalStatusPath)(gateReceiptId)}`, {
|
|
2443
|
+
method: 'GET',
|
|
2444
|
+
headers: buildHeaders(apiKey, sessionId, undefined, agentId),
|
|
2445
|
+
signal,
|
|
2446
|
+
}, {
|
|
2447
|
+
consumeResponse: async (response) => {
|
|
2448
|
+
if (response.status === 404) {
|
|
2449
|
+
await response.body?.cancel().catch(() => undefined);
|
|
2450
|
+
return { kind: 'not_found', status: null };
|
|
2451
|
+
}
|
|
2452
|
+
const json = await safeJsonResponse(response);
|
|
2453
|
+
const status = normalizeOwnerApprovalStatus(json.data, gateReceiptId);
|
|
2454
|
+
if (!status)
|
|
2455
|
+
throw (0, request_reliability_1.invalidResponseError)();
|
|
2456
|
+
return { kind: 'found', status };
|
|
2457
|
+
},
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
function normalizeHostApprovalReceipt(value, gateReceiptId) {
|
|
2461
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
2462
|
+
return null;
|
|
2463
|
+
const receipt = value;
|
|
2464
|
+
if (receipt.gate_receipt_id !== gateReceiptId
|
|
2465
|
+
|| (receipt.verdict !== 'approved' && receipt.verdict !== 'declined')
|
|
2466
|
+
|| receipt.source !== 'host_prompt' || receipt.trust !== 'client_attested')
|
|
2467
|
+
return null;
|
|
2468
|
+
return {
|
|
2469
|
+
owner_approval_receipt_id: SAFE_STATUS_ID(receipt.owner_approval_receipt_id),
|
|
2470
|
+
owner_decline_receipt_id: SAFE_STATUS_ID(receipt.owner_decline_receipt_id),
|
|
2471
|
+
gate_receipt_id: gateReceiptId,
|
|
2472
|
+
decision_id: SAFE_STATUS_ID(receipt.decision_id),
|
|
2473
|
+
verdict: receipt.verdict,
|
|
2474
|
+
source: 'host_prompt',
|
|
2475
|
+
trust: 'client_attested',
|
|
2476
|
+
answered_by: receipt.answered_by === 'host_operator' || receipt.answered_by === 'owner_chat_preapproval' ? receipt.answered_by : 'host_allow_rule',
|
|
2477
|
+
host: SAFE_STATUS_TEXT(receipt.host, 32) || 'other',
|
|
2478
|
+
recorded_at: SAFE_STATUS_TIME(receipt.recorded_at) || '',
|
|
2479
|
+
expires_at: SAFE_STATUS_TIME(receipt.expires_at) || '',
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* POST /v1/agent/gate-receipts/:id/approval-link: asks Marrow to send the
|
|
2484
|
+
* account owner a one-tap approval link for one held receipt, to the owner's
|
|
2485
|
+
* own channel. The response never contains the link; the owner approves
|
|
2486
|
+
* without a login. Returns the channel only (no recipient details).
|
|
2487
|
+
*/
|
|
2488
|
+
async function marrowRequestApprovalLink(apiKey, baseUrl, gateReceiptId, decisionId, sessionId, agentId, signal, options = {}) {
|
|
2489
|
+
if (!GATE_RECEIPT_IDENTIFIER.test(gateReceiptId))
|
|
2490
|
+
throw new TypeError('gate_receipt_id is not a valid gate receipt identifier.');
|
|
2491
|
+
const body = decisionId && GATE_RECEIPT_IDENTIFIER.test(decisionId) ? { decision_id: decisionId } : {};
|
|
2492
|
+
if (typeof options.personPresent === 'boolean')
|
|
2493
|
+
body.person_present = options.personPresent;
|
|
2494
|
+
const result = await requestApprovalLinkOnce(apiKey, baseUrl, gateReceiptId, body, sessionId, agentId, signal);
|
|
2495
|
+
// A service before round 6 refuses person_present as an unknown field: send the request without it.
|
|
2496
|
+
if (!result.ok && 'person_present' in body && result.status === 400 && result.code === 'MARROW_APPROVAL_LINK_INVALID' && result.fields?.includes('person_present')) {
|
|
2497
|
+
const { person_present: _omitted, ...plain } = body;
|
|
2498
|
+
return requestApprovalLinkOnce(apiKey, baseUrl, gateReceiptId, plain, sessionId, agentId, signal);
|
|
2499
|
+
}
|
|
2500
|
+
return result;
|
|
2501
|
+
}
|
|
2502
|
+
async function requestApprovalLinkOnce(apiKey, baseUrl, gateReceiptId, body, sessionId, agentId, signal) {
|
|
2503
|
+
return fetch(`${baseUrl}${(0, runtime_contract_1.approvalLinkPath)(gateReceiptId)}`, {
|
|
2504
|
+
method: 'POST',
|
|
2505
|
+
headers: buildHeaders(apiKey, sessionId, 'application/json', agentId),
|
|
2506
|
+
body: JSON.stringify(body),
|
|
2507
|
+
signal,
|
|
2508
|
+
}, {
|
|
2509
|
+
retryOwner: 'caller',
|
|
2510
|
+
consumeResponse: async (response) => {
|
|
2511
|
+
let json = null;
|
|
2512
|
+
try {
|
|
2513
|
+
if ((response.headers.get('content-type') || '').includes('json'))
|
|
2514
|
+
json = await response.json();
|
|
2515
|
+
else
|
|
2516
|
+
await response.body?.cancel().catch(() => undefined);
|
|
2517
|
+
}
|
|
2518
|
+
catch (error) {
|
|
2519
|
+
const failure = (0, request_reliability_1.normalizeRequestError)(error);
|
|
2520
|
+
if (failure.code === 'request_timeout')
|
|
2521
|
+
throw failure;
|
|
2522
|
+
json = null;
|
|
2523
|
+
}
|
|
2524
|
+
if (response.ok) {
|
|
2525
|
+
const data = json?.data && typeof json.data === 'object' ? json.data : null;
|
|
2526
|
+
// Quiet by default: the service sent nothing (owner_ping_off), stored nothing, counted nothing.
|
|
2527
|
+
if (data?.sent === false && data.state === 'not_sent') {
|
|
2528
|
+
const reason = typeof data.reason === 'string' && /^[a-z][a-z0-9_]{0,63}$/.test(data.reason) ? data.reason : 'not_sent';
|
|
2529
|
+
return { ok: false, status: response.status, code: reason, retryable: false, notSent: true };
|
|
2530
|
+
}
|
|
2531
|
+
const link = data?.approval_link && typeof data.approval_link === 'object' ? data.approval_link : null;
|
|
2532
|
+
// One live link per arbitration: a repeat names that link and sends nothing new.
|
|
2533
|
+
const alreadySent = data?.sent === false && data.state === 'already_sent';
|
|
2534
|
+
if (!link || (link.gate_receipt_id !== undefined && link.gate_receipt_id !== gateReceiptId) || (!alreadySent && link.gate_receipt_id !== gateReceiptId))
|
|
2535
|
+
throw (0, request_reliability_1.invalidResponseError)();
|
|
2536
|
+
return { ok: true, ...(alreadySent ? { alreadySent: true } : {}), link: {
|
|
2537
|
+
channel: typeof link.channel === 'string' && /^[a-z][a-z0-9_-]{0,31}$/.test(link.channel) ? link.channel : 'owner channel',
|
|
2538
|
+
expires_at: SAFE_STATUS_TIME(link.expires_at),
|
|
2539
|
+
} };
|
|
2540
|
+
}
|
|
2541
|
+
const details = json?.details && typeof json.details === 'object' && !Array.isArray(json.details)
|
|
2542
|
+
? json.details
|
|
2543
|
+
: {};
|
|
2544
|
+
const code = typeof details.code === 'string' && /^[A-Za-z0-9_.:-]{1,80}$/.test(details.code) ? details.code : null;
|
|
2545
|
+
const fields = Array.isArray(details.fields) ? details.fields.filter((field) => typeof field === 'string').slice(0, 10) : undefined;
|
|
2546
|
+
return { ok: false, status: response.status, code, retryable: response.status === 429 || response.status >= 500 || details.retryable === true, ...(fields ? { fields } : {}) };
|
|
2547
|
+
},
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* GET /v1/agent/held-actions: the held actions still waiting for a person, for
|
|
2552
|
+
* the next interactive session's "N held actions are waiting for you". null
|
|
2553
|
+
* on a service without the read (404).
|
|
2554
|
+
*/
|
|
2555
|
+
async function marrowHeldActions(apiKey, baseUrl, query = {}, sessionId, agentId, signal) {
|
|
2556
|
+
const params = new URLSearchParams({ scope: query.scope || 'agent', limit: String(Math.min(Math.max(Math.trunc(query.limit || 20), 1), 50)) });
|
|
2557
|
+
return fetch(`${baseUrl}${exports.HELD_ACTIONS_PATH}?${params}`, {
|
|
2558
|
+
method: 'GET',
|
|
2559
|
+
headers: buildHeaders(apiKey, sessionId, undefined, agentId),
|
|
2560
|
+
signal,
|
|
2561
|
+
}, {
|
|
2562
|
+
retryOwner: 'caller',
|
|
2563
|
+
consumeResponse: async (response) => {
|
|
2564
|
+
if (response.status === 404) {
|
|
2565
|
+
await response.body?.cancel().catch(() => undefined);
|
|
2566
|
+
return null;
|
|
2567
|
+
}
|
|
2568
|
+
if (!response.ok)
|
|
2569
|
+
throw await (0, request_reliability_1.requestErrorFromResponse)(response);
|
|
2570
|
+
const json = await safeJsonResponse(response);
|
|
2571
|
+
const data = json.data && typeof json.data === 'object' ? json.data : null;
|
|
2572
|
+
if (!data || !Array.isArray(data.holds))
|
|
2573
|
+
throw (0, request_reliability_1.invalidResponseError)();
|
|
2574
|
+
const holds = data.holds.slice(0, 50).flatMap((item) => {
|
|
2575
|
+
if (!item || typeof item !== 'object')
|
|
2576
|
+
return [];
|
|
2577
|
+
const hold = item;
|
|
2578
|
+
const id = SAFE_STATUS_ID(hold.gate_receipt_id);
|
|
2579
|
+
if (!id)
|
|
2580
|
+
return [];
|
|
2581
|
+
return [{
|
|
2582
|
+
gate_receipt_id: id,
|
|
2583
|
+
agent_id: SAFE_STATUS_ID(hold.agent_id),
|
|
2584
|
+
decision_type: typeof hold.decision_type === 'string' && /^[A-Za-z0-9][A-Za-z0-9 _.:-]{0,63}$/.test(hold.decision_type) ? hold.decision_type : null,
|
|
2585
|
+
age_seconds: typeof hold.age_seconds === 'number' && Number.isFinite(hold.age_seconds) ? Math.max(0, Math.round(hold.age_seconds)) : 0,
|
|
2586
|
+
expired: hold.expired === true,
|
|
2587
|
+
}];
|
|
2588
|
+
});
|
|
2589
|
+
const count = typeof data.count === 'number' && Number.isInteger(data.count) && data.count >= holds.length ? Math.min(data.count, 500) : holds.length;
|
|
2590
|
+
return { count, countCapped: data.count_capped === true, more: data.more === true, holds };
|
|
2591
|
+
},
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
/**
|
|
2595
|
+
* The account's owner-locked categories, for keeping those actions held on a
|
|
2596
|
+
* machine that has not seen them yet while Marrow cannot be reached. Agent
|
|
2597
|
+
* key; null on a service without the read (404). Proposed route:
|
|
2598
|
+
* GET /v1/agent/approval-settings -> { verified_approval_categories, unattended_owner_ping }.
|
|
2599
|
+
*/
|
|
2600
|
+
async function marrowAgentApprovalSettings(apiKey, baseUrl, sessionId, agentId, signal) {
|
|
2601
|
+
return fetch(`${baseUrl}${exports.AGENT_APPROVAL_SETTINGS_PATH}`, {
|
|
2602
|
+
method: 'GET',
|
|
2603
|
+
headers: buildHeaders(apiKey, sessionId, undefined, agentId),
|
|
2604
|
+
signal,
|
|
2605
|
+
}, {
|
|
2606
|
+
retryOwner: 'caller',
|
|
2607
|
+
consumeResponse: async (response) => {
|
|
2608
|
+
if (response.status === 404) {
|
|
2609
|
+
await response.body?.cancel().catch(() => undefined);
|
|
2610
|
+
return null;
|
|
2611
|
+
}
|
|
2612
|
+
if (!response.ok)
|
|
2613
|
+
throw await (0, request_reliability_1.requestErrorFromResponse)(response);
|
|
2614
|
+
const json = await safeJsonResponse(response);
|
|
2615
|
+
const data = json.data && typeof json.data === 'object' ? json.data : null;
|
|
2616
|
+
if (!data || !Array.isArray(data.verified_approval_categories))
|
|
2617
|
+
throw (0, request_reliability_1.invalidResponseError)();
|
|
2618
|
+
return {
|
|
2619
|
+
verified_approval_categories: data.verified_approval_categories
|
|
2620
|
+
.filter((item) => typeof item === 'string' && /^[a-z][a-z0-9_]{0,63}$/.test(item)).slice(0, 16),
|
|
2621
|
+
unattended_owner_ping: typeof data.unattended_owner_ping === 'boolean' ? data.unattended_owner_ping : null,
|
|
2622
|
+
};
|
|
2623
|
+
},
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
exports.HELD_ACTIONS_PATH = '/v1/agent/held-actions';
|
|
2627
|
+
exports.AGENT_APPROVAL_SETTINGS_PATH = '/v1/agent/approval-settings';
|
|
2628
|
+
const HOST_APPROVAL_REPORT_FIELDS = ['verdict', 'host', 'host_session_id', 'hook_event', 'pre_action_event_id', 'asked_at', 'answered_at', 'decision_id', 'normalized_action'];
|
|
2629
|
+
/**
|
|
2630
|
+
* POST /v1/agent/gate-receipts/:id/host-approval: the host's Marrow hook
|
|
2631
|
+
* records the operator's answer in the host's own permission prompt (or a
|
|
2632
|
+
* typed reply). Recorded client-attested; the server labels it an operator
|
|
2633
|
+
* answer only with a dialog or typed-reply marker at a human pace. Hooks call
|
|
2634
|
+
* this only for an answer the host actually reported; the model never does.
|
|
2635
|
+
*/
|
|
2636
|
+
async function marrowHostApproval(apiKey, baseUrl, gateReceiptId, report, sessionId, agentId, signal) {
|
|
2637
|
+
if (!GATE_RECEIPT_IDENTIFIER.test(gateReceiptId))
|
|
2638
|
+
throw new TypeError('gate_receipt_id is not a valid gate receipt identifier.');
|
|
2639
|
+
const body = {};
|
|
2640
|
+
for (const field of HOST_APPROVAL_REPORT_FIELDS) {
|
|
2641
|
+
const value = report[field];
|
|
2642
|
+
if ((field === 'decision_id' || field === 'normalized_action') && (value === undefined || value === null))
|
|
2643
|
+
continue;
|
|
2644
|
+
body[field] = value ?? null;
|
|
2645
|
+
}
|
|
2646
|
+
return fetch(`${baseUrl}${(0, runtime_contract_1.hostApprovalPath)(gateReceiptId)}`, {
|
|
2647
|
+
method: 'POST',
|
|
2648
|
+
headers: buildHeaders(apiKey, sessionId, 'application/json', agentId),
|
|
2649
|
+
body: JSON.stringify(body),
|
|
2650
|
+
signal,
|
|
2651
|
+
}, {
|
|
2652
|
+
// The caller resends the identical report; the server replays it.
|
|
2653
|
+
retryOwner: 'caller',
|
|
2654
|
+
consumeResponse: async (response) => {
|
|
2655
|
+
let json = null;
|
|
2656
|
+
try {
|
|
2657
|
+
if ((response.headers.get('content-type') || '').includes('json'))
|
|
2658
|
+
json = await response.json();
|
|
2659
|
+
else
|
|
2660
|
+
await response.body?.cancel().catch(() => undefined);
|
|
2661
|
+
}
|
|
2662
|
+
catch (error) {
|
|
2663
|
+
const failure = (0, request_reliability_1.normalizeRequestError)(error);
|
|
2664
|
+
if (failure.code === 'request_timeout')
|
|
2665
|
+
throw failure;
|
|
2666
|
+
json = null;
|
|
2667
|
+
}
|
|
2668
|
+
if (response.ok) {
|
|
2669
|
+
const data = json?.data && typeof json.data === 'object' ? json.data : null;
|
|
2670
|
+
const receipt = normalizeHostApprovalReceipt(data?.host_approval, gateReceiptId);
|
|
2671
|
+
if (!receipt)
|
|
2672
|
+
throw (0, request_reliability_1.invalidResponseError)();
|
|
2673
|
+
return { ok: true, receipt, replayed: data?.replayed === true };
|
|
2674
|
+
}
|
|
2675
|
+
const details = json?.details && typeof json.details === 'object' && !Array.isArray(json.details)
|
|
2676
|
+
? json.details
|
|
2677
|
+
: {};
|
|
2678
|
+
const code = typeof details.code === 'string' && /^[A-Za-z0-9_.:-]{1,80}$/.test(details.code) ? details.code : null;
|
|
2679
|
+
const existing = details.existing_verdict === 'approved' || details.existing_verdict === 'declined' ? details.existing_verdict : null;
|
|
2680
|
+
const retryAfter = (0, request_reliability_1.responseRetryAfter)(response);
|
|
2681
|
+
const fields = Array.isArray(details.fields)
|
|
2682
|
+
? details.fields.filter((item) => typeof item === 'string' && /^[a-z_]{1,64}$/.test(item)).slice(0, 10)
|
|
2683
|
+
: [];
|
|
2684
|
+
return {
|
|
2685
|
+
ok: false,
|
|
2686
|
+
status: response.status,
|
|
2687
|
+
code,
|
|
2688
|
+
existingVerdict: existing,
|
|
2689
|
+
retryable: response.status === 429 || response.status >= 500 || details.retryable === true,
|
|
2690
|
+
retryAfterMs: retryAfter.valid ? retryAfter.delayMs : null,
|
|
2691
|
+
...(fields.length ? { fields } : {}),
|
|
2692
|
+
};
|
|
2693
|
+
},
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
1981
2696
|
async function marrowEnforcement(apiKey, baseUrl, input, sessionId, agentId, signal) {
|
|
1982
2697
|
const body = {
|
|
1983
2698
|
...input,
|