@sema-agent/core 7.26.3 → 7.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/dist/agents/builtin-agents.d.ts +6 -15
- package/dist/agents/cascade.d.ts +29 -33
- package/dist/agents/observer.d.ts +18 -20
- package/dist/agents/subagent.d.ts +58 -70
- package/dist/agents/tool-filter.d.ts +10 -12
- package/dist/agents/verify.d.ts +44 -49
- package/dist/brain/input-too-long.d.ts +10 -12
- package/dist/brain/media-degrade.d.ts +4 -5
- package/dist/brain/reasoning.d.ts +10 -12
- package/dist/brain/repetition.d.ts +7 -9
- package/dist/brain/request-params.d.ts +20 -25
- package/dist/brain/route-adjudicator.d.ts +4 -4
- package/dist/brain/route-conformance.d.ts +2 -2
- package/dist/brain/status-sink.d.ts +12 -14
- package/dist/brain/terminal-cause.d.ts +12 -15
- package/dist/brain/timeout.d.ts +17 -19
- package/dist/core/auto-mode-directive.d.ts +6 -4
- package/dist/core/auto-mode-directive.js +1 -5
- package/dist/core/engine-notice.d.ts +13 -6
- package/dist/core/memory-engine/consolidation.d.ts +11 -15
- package/dist/core/memory-engine/engine.d.ts +45 -54
- package/dist/core/memory-engine/tools.d.ts +17 -23
- package/dist/core/permission-rule-model.js +5 -3
- package/dist/core/runner/contracts.d.ts +31 -7
- package/dist/core/runner/denial-limit-arms.d.ts +17 -15
- package/dist/core/runner/denial-limit-arms.js +4 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +1 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +1 -2
- package/dist/core/runner/prepare-policy-chain.d.ts +1 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/run-leg.js +5 -1
- package/dist/core/tool-policy.d.ts +23 -2
- package/dist/core/tool-policy.js +106 -103
- package/dist/index.d.ts +6 -1
- package/dist/shell/lex.d.ts +279 -0
- package/dist/shell/lex.js +1253 -0
- package/dist/stores/file/background-agent-store.d.ts +4 -0
- package/dist/stores/file/background-agent-store.js +10 -0
- package/dist/stores/file/checkpoint-store.d.ts +3 -0
- package/dist/stores/file/checkpoint-store.js +18 -0
- package/dist/stores/file/corrupt-read-sink.d.ts +20 -0
- package/dist/stores/file/corrupt-read-sink.js +15 -0
- package/dist/stores/file/file-history-store.d.ts +1 -2
- package/dist/stores/file/file-history-store.js +3 -8
- package/dist/stores/file/fs-atomic.d.ts +8 -3
- package/dist/stores/file/fs-atomic.js +19 -2
- package/dist/stores/file/index.js +4 -4
- package/dist/stores/file/mailbox-store.d.ts +3 -0
- package/dist/stores/file/mailbox-store.js +24 -18
- package/dist/stores/file/permission-rule-store.js +18 -1
- package/dist/stores/file/session-policy-store.d.ts +17 -1
- package/dist/stores/file/session-policy-store.js +3 -12
- package/dist/stores/file/session-store.d.ts +1 -5
- package/dist/stores/file/session-store.js +9 -14
- package/dist/stores/file/shared-ledger.d.ts +18 -0
- package/dist/stores/file/shared-ledger.js +16 -2
- package/dist/stores/file/tool-result-store.d.ts +9 -1
- package/dist/stores/file/tool-result-store.js +17 -2
- package/dist/stores/file/workflow-journal-store.d.ts +10 -1
- package/dist/stores/file/workflow-journal-store.js +8 -12
- package/dist/stores/file/workflow-run-store.d.ts +3 -0
- package/dist/stores/file/workflow-run-store.js +10 -0
- package/dist/tools/artifact/local-stub.js +16 -1
- package/dist/tools/fs/bash-program-position.d.ts +2 -4
- package/dist/tools/fs/bash-program-position.js +2 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
- package/dist/tools/fs/bash-readonly-classifier.js +1 -1
- package/dist/tools/fs/shell-read-check.js +1 -1
- package/dist/tools/fs/shell-read-establish.js +11 -1
- package/dist/tools/fs/shell-read-plan.d.ts +7 -2
- package/dist/tools/fs/shell-read-plan.js +48 -10
- package/package.json +2 -2
- package/test/export-surface.snapshot.json +3 -1
- package/wire-changes/core@7.27.0.json +152 -0
- package/wire-changes/core@7.28.0.json +594 -0
- package/dist/tools/fs/bash-lexer.d.ts +0 -152
- package/dist/tools/fs/bash-lexer.js +0 -627
package/dist/core/tool-policy.js
CHANGED
|
@@ -414,11 +414,31 @@ export function createApprovalPolicy(opts) {
|
|
|
414
414
|
if (signal?.aborted) {
|
|
415
415
|
return attest({ action: "deny", message: `approval aborted for "${req.toolName}" (run or turn interrupted)` }, settledByNobody("task_aborted"));
|
|
416
416
|
}
|
|
417
|
+
if (typeof ok === "object" && ok !== null) {
|
|
418
|
+
const reading = readApproverOutcomeObject(ok, req.toolName, "none");
|
|
419
|
+
if (reading.kind === "refused")
|
|
420
|
+
return attest({ action: "deny", message: reading.message }, reading.settlement);
|
|
421
|
+
if (reading.kind === "deny") {
|
|
422
|
+
return attest({
|
|
423
|
+
action: "deny",
|
|
424
|
+
message: reading.word === "policy"
|
|
425
|
+
? policyRefusalMessage(req.toolName, undefined, reading.note)
|
|
426
|
+
: reading.word === "timeout"
|
|
427
|
+
? `no one answered the approval request for "${req.toolName}" — the approver reported its own window elapsed with no answer; denied fail-closed${reading.note !== undefined ? `\nReviewer note:\n${delimitUntrusted("reviewer note", reading.note, REVIEWER_NOTE_MAX_BODY)}` : ""}`
|
|
428
|
+
: `approval denied for "${req.toolName}"${reading.note !== undefined ? `\nReviewer note:\n${delimitUntrusted("reviewer note", reading.note, REVIEWER_NOTE_MAX_BODY)}` : ""}`,
|
|
429
|
+
}, reading.settlement);
|
|
430
|
+
}
|
|
431
|
+
return attest({ action: "allow" }, reading.settlement);
|
|
432
|
+
}
|
|
417
433
|
const okRaw = ok;
|
|
418
434
|
if (okRaw === true)
|
|
419
435
|
return attest({ action: "allow" }, settledByPerson("human_allowed", undefined));
|
|
420
436
|
if (okRaw !== false) {
|
|
421
|
-
return attest({
|
|
437
|
+
return attest({
|
|
438
|
+
action: "deny",
|
|
439
|
+
message: `approval callback for "${req.toolName}" returned an out-of-contract value (${typeof ok}) — denied fail-closed ` +
|
|
440
|
+
`(this policy's \`approve\` returns true, false, or the {allow, settledBy?, reason?, approver?} outcome object)`,
|
|
441
|
+
}, settledByNobody("approver_contract"));
|
|
422
442
|
}
|
|
423
443
|
return attest({ action: "deny", message: `approval denied for "${req.toolName}"` }, settledByPerson("human_refused", undefined));
|
|
424
444
|
}
|
|
@@ -1134,6 +1154,78 @@ function detachLateAskWait(wait, onLate) {
|
|
|
1134
1154
|
})
|
|
1135
1155
|
.catch(() => undefined);
|
|
1136
1156
|
}
|
|
1157
|
+
function readApproverOutcomeObject(ok, toolName, editLane) {
|
|
1158
|
+
const refused = (message, settlement) => ({ kind: "refused", message, settlement });
|
|
1159
|
+
let supplied;
|
|
1160
|
+
let allowed;
|
|
1161
|
+
let suppliedEdit;
|
|
1162
|
+
let suppliedApprover;
|
|
1163
|
+
try {
|
|
1164
|
+
supplied = ok.settledBy;
|
|
1165
|
+
allowed = ok.allow;
|
|
1166
|
+
suppliedEdit = ok.updatedInput;
|
|
1167
|
+
suppliedApprover = ok.approver;
|
|
1168
|
+
}
|
|
1169
|
+
catch (err) {
|
|
1170
|
+
return refused(`the approver's outcome for "${toolName}" could not be read (${describeThrown(err)}) — denied fail-closed`, settledByNobody("approver_contract"));
|
|
1171
|
+
}
|
|
1172
|
+
if (typeof allowed !== "boolean") {
|
|
1173
|
+
return refused(`the approver for "${toolName}" returned an object whose allow is not a boolean ` +
|
|
1174
|
+
`(got ${allowed === null ? "null" : typeof allowed}) — a verdict is exactly true or false; denied fail-closed`, settledByNobody("approver_contract"));
|
|
1175
|
+
}
|
|
1176
|
+
if (supplied !== undefined && supplied !== "human" && supplied !== "timeout" && supplied !== "policy") {
|
|
1177
|
+
return refused(`the approver for "${toolName}" reported settledBy "${typeof supplied === "string" ? containThrownText(supplied) : supplied === null ? "null" : typeof supplied}", which is outside what a synchronous ` +
|
|
1178
|
+
`approver may self-report — it is exactly "human", "timeout" or "policy" (or omitted); denied fail-closed`, settledByNobody("approver_contract"));
|
|
1179
|
+
}
|
|
1180
|
+
const settledWord = supplied === "timeout" ? "timeout" : supplied === "policy" ? "policy" : "human";
|
|
1181
|
+
const attribution = screenApproverAttribution(suppliedApprover);
|
|
1182
|
+
if (attribution.defect !== undefined) {
|
|
1183
|
+
return refused(`the approver for "${toolName}" reported an attribution this seam refuses: ${attribution.defect}; denied fail-closed`, settledByNobody("approver_contract"));
|
|
1184
|
+
}
|
|
1185
|
+
if (settledWord !== "human" && allowed === true) {
|
|
1186
|
+
return refused(`the approver for "${toolName}" returned an allow settled by "${settledWord}" — only a person approves, and that word ` +
|
|
1187
|
+
`names a wait that ended with nobody judging: it cannot be what approved a call; denied fail-closed ` +
|
|
1188
|
+
`(report "${settledWord}" with allow:false, or allow with settledBy "human"/omitted)`, settledByNobody("approver_contract"));
|
|
1189
|
+
}
|
|
1190
|
+
if (allowed !== true) {
|
|
1191
|
+
let reasonText;
|
|
1192
|
+
try {
|
|
1193
|
+
const suppliedReason = ok.reason;
|
|
1194
|
+
if (suppliedReason !== undefined && typeof suppliedReason !== "string") {
|
|
1195
|
+
return refused(`the approver for "${toolName}" attached a reason that is not a plain string ` +
|
|
1196
|
+
`(got ${suppliedReason === null ? "null" : typeof suppliedReason}) — a deny note is the decider's plain text; denied fail-closed`, settledByNobody("approver_contract"));
|
|
1197
|
+
}
|
|
1198
|
+
reasonText = typeof suppliedReason === "string" ? suppliedReason : undefined;
|
|
1199
|
+
}
|
|
1200
|
+
catch (err) {
|
|
1201
|
+
return refused(`the approver for "${toolName}" attached a reason that could not be read (${describeThrown(err)}) — denied fail-closed`, settledByNobody("approver_contract"));
|
|
1202
|
+
}
|
|
1203
|
+
const note = decidersNote(reasonText);
|
|
1204
|
+
return {
|
|
1205
|
+
kind: "deny",
|
|
1206
|
+
word: settledWord,
|
|
1207
|
+
note,
|
|
1208
|
+
settlement: settledWord === "timeout"
|
|
1209
|
+
? { kind: "approval_window_expired", who: { party: "host" }, when: Date.now() }
|
|
1210
|
+
: settledWord === "policy"
|
|
1211
|
+
? settledByNobody("policy_refused")
|
|
1212
|
+
: settledByPerson("human_refused", attribution.approver, note),
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
if (suppliedEdit === undefined)
|
|
1216
|
+
return { kind: "allow", edit: undefined, settlement: settledByPerson("human_allowed", attribution.approver) };
|
|
1217
|
+
if (editLane === "none") {
|
|
1218
|
+
return refused(`the approver for "${toolName}" attached an edit (updatedInput) on a seat that has no lane to adjudicate one — ` +
|
|
1219
|
+
`this seat's answer is the deployment POLICY's verdict, and an edit arriving through it would execute without the ` +
|
|
1220
|
+
`re-adjudication an approver's edit gets; denied fail-closed (approve or refuse what was shown, and route edits ` +
|
|
1221
|
+
`through the run's approver seat)`, settledByNobody("approver_contract"));
|
|
1222
|
+
}
|
|
1223
|
+
const edit = tryCloneArgs(suppliedEdit);
|
|
1224
|
+
if (!edit.ok) {
|
|
1225
|
+
return refused(`the approved edit for "${toolName}" is not safely clonable (${edit.reason}) — denied fail-closed`, settledByNobody("presentation_failed"));
|
|
1226
|
+
}
|
|
1227
|
+
return { kind: "allow", edit: { value: edit.value }, settlement: settledByPerson("human_allowed", attribution.approver) };
|
|
1228
|
+
}
|
|
1137
1229
|
async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
1138
1230
|
if (onAsk === "allow") {
|
|
1139
1231
|
if (req.requiresRealApproval === true) {
|
|
@@ -1229,113 +1321,24 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1229
1321
|
};
|
|
1230
1322
|
}
|
|
1231
1323
|
if (typeof ok === "object" && ok !== null) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
try {
|
|
1237
|
-
supplied = ok.settledBy;
|
|
1238
|
-
allowed = ok.allow;
|
|
1239
|
-
suppliedEdit = ok.updatedInput;
|
|
1240
|
-
suppliedApprover = ok.approver;
|
|
1241
|
-
}
|
|
1242
|
-
catch (err) {
|
|
1243
|
-
return {
|
|
1244
|
-
action: "deny",
|
|
1245
|
-
message: `the approver's outcome for "${req.toolName}" could not be read (${describeThrown(err)}) — denied fail-closed`,
|
|
1246
|
-
decisionReason: "mode",
|
|
1247
|
-
settlement: settledByNobody("approver_contract"),
|
|
1248
|
-
};
|
|
1249
|
-
}
|
|
1250
|
-
if (typeof allowed !== "boolean") {
|
|
1324
|
+
const reading = readApproverOutcomeObject(ok, req.toolName, "readjudicated");
|
|
1325
|
+
if (reading.kind === "refused")
|
|
1326
|
+
return { action: "deny", message: reading.message, decisionReason: "mode", settlement: reading.settlement };
|
|
1327
|
+
if (reading.kind === "deny") {
|
|
1251
1328
|
return {
|
|
1252
1329
|
action: "deny",
|
|
1253
|
-
message:
|
|
1254
|
-
`
|
|
1330
|
+
message: reading.word === "timeout"
|
|
1331
|
+
? `approval for "${req.toolName}" was not answered before the approver's own window elapsed: ${req.message}${reading.note !== undefined ? `\nReviewer note:\n${delimitUntrusted("reviewer note", reading.note, REVIEWER_NOTE_MAX_BODY)}` : ""}`
|
|
1332
|
+
: reading.word === "policy"
|
|
1333
|
+
? policyRefusalMessage(req.toolName, req.message, reading.note)
|
|
1334
|
+
: humanRefusalMessage(req, reading.note),
|
|
1255
1335
|
decisionReason: "mode",
|
|
1256
|
-
settlement:
|
|
1257
|
-
};
|
|
1258
|
-
}
|
|
1259
|
-
if (supplied !== undefined && supplied !== "human" && supplied !== "timeout" && supplied !== "policy") {
|
|
1260
|
-
return {
|
|
1261
|
-
action: "deny",
|
|
1262
|
-
message: `the approver for "${req.toolName}" reported settledBy "${typeof supplied === "string" ? containThrownText(supplied) : supplied === null ? "null" : typeof supplied}", which is outside what a synchronous ` +
|
|
1263
|
-
`approver may self-report — it is exactly "human", "timeout" or "policy" (or omitted); denied fail-closed`,
|
|
1264
|
-
decisionReason: "mode",
|
|
1265
|
-
settlement: settledByNobody("approver_contract"),
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
const settledWord = supplied === "timeout" ? "timeout" : supplied === "policy" ? "policy" : "human";
|
|
1269
|
-
const attribution = screenApproverAttribution(suppliedApprover);
|
|
1270
|
-
if (attribution.defect !== undefined) {
|
|
1271
|
-
return {
|
|
1272
|
-
action: "deny",
|
|
1273
|
-
message: `the approver for "${req.toolName}" reported an attribution this seam refuses: ${attribution.defect}; denied fail-closed`,
|
|
1274
|
-
decisionReason: "mode",
|
|
1275
|
-
settlement: settledByNobody("approver_contract"),
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
if (settledWord !== "human" && allowed === true) {
|
|
1279
|
-
return {
|
|
1280
|
-
action: "deny",
|
|
1281
|
-
message: `the approver for "${req.toolName}" returned an allow settled by "${settledWord}" — only a person approves, and that word ` +
|
|
1282
|
-
`names a wait that ended with nobody judging: it cannot be what approved a call; denied fail-closed ` +
|
|
1283
|
-
`(report "${settledWord}" with allow:false, or allow with settledBy "human"/omitted)`,
|
|
1284
|
-
decisionReason: "mode",
|
|
1285
|
-
settlement: settledByNobody("approver_contract"),
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
if (allowed !== true) {
|
|
1289
|
-
let reasonText;
|
|
1290
|
-
try {
|
|
1291
|
-
const suppliedReason = ok.reason;
|
|
1292
|
-
if (suppliedReason !== undefined && typeof suppliedReason !== "string") {
|
|
1293
|
-
return {
|
|
1294
|
-
action: "deny",
|
|
1295
|
-
message: `the approver for "${req.toolName}" attached a reason that is not a plain string ` +
|
|
1296
|
-
`(got ${suppliedReason === null ? "null" : typeof suppliedReason}) — a deny note is the decider's plain text; denied fail-closed`,
|
|
1297
|
-
decisionReason: "mode",
|
|
1298
|
-
settlement: settledByNobody("approver_contract"),
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
reasonText = typeof suppliedReason === "string" ? suppliedReason : undefined;
|
|
1302
|
-
}
|
|
1303
|
-
catch (err) {
|
|
1304
|
-
return {
|
|
1305
|
-
action: "deny",
|
|
1306
|
-
message: `the approver for "${req.toolName}" attached a reason that could not be read (${describeThrown(err)}) — denied fail-closed`,
|
|
1307
|
-
decisionReason: "mode",
|
|
1308
|
-
settlement: settledByNobody("approver_contract"),
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
const note = decidersNote(reasonText);
|
|
1312
|
-
return {
|
|
1313
|
-
action: "deny",
|
|
1314
|
-
message: settledWord === "timeout"
|
|
1315
|
-
? `approval for "${req.toolName}" was not answered before the approver's own window elapsed: ${req.message}${note !== undefined ? `\nReviewer note:\n${delimitUntrusted("reviewer note", note, REVIEWER_NOTE_MAX_BODY)}` : ""}`
|
|
1316
|
-
: settledWord === "policy"
|
|
1317
|
-
? policyRefusalMessage(req.toolName, req.message, note)
|
|
1318
|
-
: humanRefusalMessage(req, note),
|
|
1319
|
-
decisionReason: "mode",
|
|
1320
|
-
settlement: settledWord === "timeout"
|
|
1321
|
-
? { kind: "approval_window_expired", who: { party: "host" }, when: Date.now() }
|
|
1322
|
-
: settledWord === "policy"
|
|
1323
|
-
? settledByNobody("policy_refused")
|
|
1324
|
-
: settledByPerson("human_refused", attribution.approver, note),
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
if (suppliedEdit === undefined)
|
|
1328
|
-
return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settlement: settledByPerson("human_allowed", attribution.approver) };
|
|
1329
|
-
const edit = tryCloneArgs(suppliedEdit);
|
|
1330
|
-
if (!edit.ok) {
|
|
1331
|
-
return {
|
|
1332
|
-
action: "deny",
|
|
1333
|
-
message: `the approved edit for "${req.toolName}" is not safely clonable (${edit.reason}) — denied fail-closed`,
|
|
1334
|
-
decisionReason: "mode",
|
|
1335
|
-
settlement: settledByNobody("presentation_failed"),
|
|
1336
|
+
settlement: reading.settlement,
|
|
1336
1337
|
};
|
|
1337
1338
|
}
|
|
1338
|
-
|
|
1339
|
+
if (reading.edit === undefined)
|
|
1340
|
+
return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settlement: reading.settlement };
|
|
1341
|
+
return { action: "allow", updatedInput: reading.edit.value, decisionReason: "mode", settlement: reading.settlement };
|
|
1339
1342
|
}
|
|
1340
1343
|
const okRaw = ok;
|
|
1341
1344
|
if (okRaw === true)
|
package/dist/index.d.ts
CHANGED
|
@@ -177,7 +177,12 @@ export type { SchedulerCapability, SchedulerErrorCode, ScheduledIntent, Schedule
|
|
|
177
177
|
export { createSchedulerTools, type SchedulerToolContext, SCHEDULE_WAKEUP_TOOL_NAME, AUTONOMOUS_LOOP_SENTINEL, AUTONOMOUS_LOOP_DYNAMIC_SENTINEL, } from "./tools/scheduler-tools.js";
|
|
178
178
|
export { resolveAutonomousLoopPrompt, AUTONOMOUS_LOOP_PREAMBLE, AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT, type AutonomousLoopPromptOptions, } from "./tools/loop-tick.js";
|
|
179
179
|
export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.js";
|
|
180
|
-
export { createAllowDenyPolicy, createApprovalPolicy,
|
|
180
|
+
export { createAllowDenyPolicy, createApprovalPolicy,
|
|
181
|
+
/** The return type of `createApprovalPolicy.approve` — a boolean, or the `AskOutcome` object minus the two
|
|
182
|
+
* members that seat has no lane for (`"unavailable"`, a routing answer; `updatedInput`, an operator edit a
|
|
183
|
+
* policy verdict cannot get re-adjudicated). Exported so a host can NAME it instead of deriving it through
|
|
184
|
+
* `Parameters<typeof createApprovalPolicy>[0]["approve"]`. */
|
|
185
|
+
type ApprovalSeatOutcome, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicyProjection, type ToolPolicyProjectionComponent, type ConstraintChainEntry, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, type AskRuleEvidence, type AskEvidenceAbsence, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
|
|
181
186
|
export { type AskOrigin, ASK_ORIGINS, isAskOrigin, classifierMayAnswer, ORIGIN_IMPLIES_REAL_APPROVAL, RULE_STORE_UNREADABLE_KINDS, type RuleStoreUnreadable, isRuleStoreUnreadable } from "./core/ask-origin.js";
|
|
182
187
|
export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource, type AutoModeDenySource } from "./core/runtime-caps.js";
|
|
183
188
|
export { SETTLEMENT_KINDS, type SettlementKind, isSettlementKind, type Settlement, SETTLEMENT_IS_REFUSAL, DENIED_BY_VALUES, type DeniedBy, isDeniedBy, DENIED_BY_MAY_VETO, CLASSIFIER_DENY_CAUSES, type ClassifierDenyCause, isClassifierDenyCause, DENIED_BY_MAY_CARRY_CAUSE, type GateDisposition, type GateOutcome, screenGateOutcome } from "./core/gate-outcome.js";
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE shell lexer: one walk over a bash command mints a structure of words made of PIECES, and every other
|
|
3
|
+
* spelling a reader consumes — `raw`, `folded`, the quoted spans, the expansion list, the assignment fact, the
|
|
4
|
+
* reason a value cannot be read off the text — is a view of that structure. Dialect: bash (the execution
|
|
5
|
+
* environment's last-resort `sh` reads `$'…'`, `+=`, a subscript, `<<<`, `<(…)` and `&>` differently; see
|
|
6
|
+
* {@link bashOnlyConstructsOf}).
|
|
7
|
+
*
|
|
8
|
+
* Two modes remain. STRICT is the residual space the classify faces live in: the caller has hard-rejected
|
|
9
|
+
* `< > $ ( ) \` \n \r \\` (or passes `<`/`>` through as text under the rule lane's tolerate option), so the whole of
|
|
10
|
+
* bash's word grammar left to model there is quote pairing, the four connectors and whitespace. LENIENT is the space
|
|
11
|
+
* the read faces read — everything the strict gate refused, RECORDED instead of refused. The lenient-only
|
|
12
|
+
* constructs are branches the strict caller can never reach, so for every strict-accepted input the two modes
|
|
13
|
+
* produce the same words and connectors, up to the strict mode's two residuals (a `#` is a word there, and JS
|
|
14
|
+
* whitespace rather than bash's blanks bounds a word).
|
|
15
|
+
*
|
|
16
|
+
* A command bash cannot parse is a {@link LexParseFailure}, never a guessed reading: an unterminated quote,
|
|
17
|
+
* substitution, subscript or here-document yields NO segments. A word bash reads but whose VALUE the text does not
|
|
18
|
+
* contain carries {@link LexedWord.unresolved}; the word is still delimited and its neighbours still read.
|
|
19
|
+
*
|
|
20
|
+
* Line continuations. Bash removes a `\<newline>` pair before it recognises anything — except inside single quotes,
|
|
21
|
+
* a comment and a QUOTED here-document body. Every lookahead below reads the next LOGICAL character past any
|
|
22
|
+
* continuation, so a pair can stand inside a construct's spelling and never inside the three places bash keeps it.
|
|
23
|
+
*/
|
|
24
|
+
export type LexMode = "strict" | "lenient";
|
|
25
|
+
export interface LexOptions {
|
|
26
|
+
readonly mode: LexMode;
|
|
27
|
+
/** `"split"` (default): the connectors bound segments. `"text"`: a single segment is being tokenized and an
|
|
28
|
+
* unquoted connector character is a word character. */
|
|
29
|
+
readonly connectors?: "split" | "text";
|
|
30
|
+
}
|
|
31
|
+
/** The dialect this lexer reads. The execution environment falls back to `sh` when no bash is found; a reader on
|
|
32
|
+
* that path must degrade every word carrying a {@link BashOnlyConstruct} to the conservative reading. */
|
|
33
|
+
export declare const SHELL_LEX_DIALECT = "bash";
|
|
34
|
+
/** What holds a character quoted: the four quote forms and the backslash escape, one reason each. */
|
|
35
|
+
export declare const QUOTE_KINDS: readonly ["'", "\"", "$'", "$\"", "\\"];
|
|
36
|
+
export type QuoteKind = (typeof QUOTE_KINDS)[number];
|
|
37
|
+
/** The expansions this lexer names. Extended globs are absent on purpose: `bash -c` runs with `extglob` off, so
|
|
38
|
+
* `?(…)` is a syntax error there and `(` is a group token here. */
|
|
39
|
+
export declare const EXPANSION_KINDS: readonly ["parameter", "command", "process", "arithmetic", "tilde", "brace", "glob"];
|
|
40
|
+
export type ExpansionKind = (typeof EXPANSION_KINDS)[number];
|
|
41
|
+
/** Why a word's value cannot be read off the text. Each member is a spelling whose value the text does not
|
|
42
|
+
* contain — never "the lexer did not look". */
|
|
43
|
+
export declare const UNRESOLVED_REASONS: readonly ["ansi_c_escape", "locale_string", "parameter_text", "subscript_text", "compound_array_value"];
|
|
44
|
+
export type UnresolvedReason = (typeof UNRESOLVED_REASONS)[number];
|
|
45
|
+
/** What the {@link lexShellText} view does with an unresolved word: `refuse` hands the consumer the reason's
|
|
46
|
+
* sentence; `fold` hands it the folded spelling as the pre-fact readers judged it. The locale string folds:
|
|
47
|
+
* refusing it would turn a deny row's HIT (judged on the untranslated spelling) into a mere question, and a
|
|
48
|
+
* reader that asks on both faces AND keeps the deny judgment reads the fact, not this view. */
|
|
49
|
+
export declare const UNRESOLVED_VIEW: {
|
|
50
|
+
readonly ansi_c_escape: "refuse";
|
|
51
|
+
readonly locale_string: "fold";
|
|
52
|
+
readonly parameter_text: "refuse";
|
|
53
|
+
readonly subscript_text: "refuse";
|
|
54
|
+
readonly compound_array_value: "refuse";
|
|
55
|
+
};
|
|
56
|
+
/** The sentence the {@link lexShellText} view hands a consumer for an unresolved word it refuses. */
|
|
57
|
+
export declare const UNRESOLVED_SENTENCE: {
|
|
58
|
+
readonly ansi_c_escape: "an ANSI-C quoted word carries an escape this lexer does not fold";
|
|
59
|
+
readonly locale_string: "a locale-translated string carries text this lexer does not read";
|
|
60
|
+
readonly parameter_text: "a parameter expansion carries text this lexer does not read";
|
|
61
|
+
readonly subscript_text: "a subscript carries text this lexer does not read";
|
|
62
|
+
readonly compound_array_value: "a compound assignment value is a list this lexer does not read";
|
|
63
|
+
};
|
|
64
|
+
/** Why a command yields no segments: bash's own syntax errors, the two constructs this lexer cannot delimit, and
|
|
65
|
+
* the strict mode's backgrounding refusal. */
|
|
66
|
+
export declare const PARSE_FAILURE_REASONS: readonly ["unterminated_single_quote", "unterminated_double_quote", "unterminated_ansi_c", "unterminated_subscript", "unterminated_compound_value", "unterminated_command_substitution", "unterminated_process_substitution", "unterminated_parameter_expansion", "unterminated_heredoc", "undelimited_substitution", "redirect_without_target", "heredoc_without_delimiter", "heredoc_delimiter_expands", "background_operator"];
|
|
67
|
+
export type ParseFailureReason = (typeof PARSE_FAILURE_REASONS)[number];
|
|
68
|
+
export declare const PARSE_FAILURE_SENTENCE: {
|
|
69
|
+
readonly unterminated_single_quote: "a single-quoted word never closes";
|
|
70
|
+
readonly unterminated_double_quote: "a double-quoted word never closes";
|
|
71
|
+
readonly unterminated_ansi_c: "an ANSI-C quoted word never closes";
|
|
72
|
+
readonly unterminated_subscript: "a subscript never closes";
|
|
73
|
+
readonly unterminated_compound_value: "a compound assignment value never closes";
|
|
74
|
+
readonly unterminated_command_substitution: "a command substitution never terminates";
|
|
75
|
+
readonly unterminated_process_substitution: "a process substitution never terminates";
|
|
76
|
+
readonly unterminated_parameter_expansion: "a parameter expansion never terminates";
|
|
77
|
+
readonly unterminated_heredoc: "a here-document body never terminates";
|
|
78
|
+
readonly undelimited_substitution: "a command substitution carries a construct this lexer cannot delimit";
|
|
79
|
+
readonly redirect_without_target: "a redirection names no target";
|
|
80
|
+
readonly heredoc_without_delimiter: "a here-document operator names no delimiter";
|
|
81
|
+
readonly heredoc_delimiter_expands: "a here-document delimiter is not a plain word";
|
|
82
|
+
readonly background_operator: "`&` backgrounding is not allowed — a backgrounded process outlives the command";
|
|
83
|
+
};
|
|
84
|
+
/** The constructs of this dialect a POSIX `sh` reads as something else. */
|
|
85
|
+
export declare const BASH_ONLY_CONSTRUCTS: readonly ["ansi_c_quoting", "locale_string", "append_assignment", "subscript_assignment", "compound_assignment", "here_string", "process_substitution", "redirect_both", "brace_expansion"];
|
|
86
|
+
export type BashOnlyConstruct = (typeof BASH_ONLY_CONSTRUCTS)[number];
|
|
87
|
+
/**
|
|
88
|
+
* The ANSI-C escapes this lexer folds: the thirteen single-letter ones, whose byte is the same in every bash and
|
|
89
|
+
* in the reference implementation's table. Numeric, control and unicode escapes are NOT here — their bytes differ
|
|
90
|
+
* between bash 3.2 and 5.x (`\c?`, `\u`, `\U`) or truncate the region (NUL) — and a body carrying one is
|
|
91
|
+
* `unresolved` rather than decoded. Admission to this table is a differential test against the real shells.
|
|
92
|
+
*/
|
|
93
|
+
export declare const ANSI_C_ESCAPES: {
|
|
94
|
+
readonly a: "\u0007";
|
|
95
|
+
readonly b: "\b";
|
|
96
|
+
readonly e: "\u001B";
|
|
97
|
+
readonly E: "\u001B";
|
|
98
|
+
readonly f: "\f";
|
|
99
|
+
readonly n: "\n";
|
|
100
|
+
readonly r: "\r";
|
|
101
|
+
readonly t: "\t";
|
|
102
|
+
readonly v: "\v";
|
|
103
|
+
readonly "\\": "\\";
|
|
104
|
+
readonly "'": "'";
|
|
105
|
+
readonly '"': "\"";
|
|
106
|
+
readonly "?": "?";
|
|
107
|
+
};
|
|
108
|
+
export interface WordPiece {
|
|
109
|
+
/** Source offsets of the written text, `[start, end)`; a continuation between two pieces is nobody's. */
|
|
110
|
+
readonly span: readonly [number, number];
|
|
111
|
+
readonly quotedBy: QuoteKind | undefined;
|
|
112
|
+
/** The text the program receives from this piece — absent for an expansion and for an unresolved piece. */
|
|
113
|
+
readonly literal?: string;
|
|
114
|
+
readonly expansion?: WordExpansion;
|
|
115
|
+
}
|
|
116
|
+
export interface WordExpansion {
|
|
117
|
+
readonly kind: ExpansionKind;
|
|
118
|
+
/** `"many"` where the result is word-split (an unquoted parameter or command substitution, a brace list, a glob). */
|
|
119
|
+
readonly yields: "one" | "many";
|
|
120
|
+
/** The text between the delimiters, where there is one (a substitution body, a brace list, a tilde-prefix). */
|
|
121
|
+
readonly body?: string;
|
|
122
|
+
/** The parameter name, where the expansion names one. */
|
|
123
|
+
readonly name?: string;
|
|
124
|
+
}
|
|
125
|
+
export interface QuotedSpan {
|
|
126
|
+
/** Offsets into {@link LexedWord.raw}, `[start, end)`; the quote characters belong to their span. */
|
|
127
|
+
readonly start: number;
|
|
128
|
+
readonly end: number;
|
|
129
|
+
readonly by: QuoteKind;
|
|
130
|
+
}
|
|
131
|
+
/** Bash's assignment fact for a word whose folded spelling is `NAME[subscript]?(+)?=value`, minted wherever the
|
|
132
|
+
* word stands. Whether bash STRIPS it before choosing the program is `bare && atCommandPosition`; whether the
|
|
133
|
+
* program that receives it reads it as an assignment is the program's own grammar. */
|
|
134
|
+
export interface AssignmentFact {
|
|
135
|
+
readonly name: string;
|
|
136
|
+
readonly op: "=" | "+=";
|
|
137
|
+
readonly subscript?: {
|
|
138
|
+
readonly raw: string;
|
|
139
|
+
readonly folded: string;
|
|
140
|
+
readonly expansions: readonly WordPiece[];
|
|
141
|
+
};
|
|
142
|
+
readonly value: {
|
|
143
|
+
readonly raw: string;
|
|
144
|
+
readonly folded: string;
|
|
145
|
+
readonly pieces: readonly WordPiece[];
|
|
146
|
+
readonly expansions: readonly WordPiece[];
|
|
147
|
+
readonly unresolved?: UnresolvedReason;
|
|
148
|
+
};
|
|
149
|
+
/** The name, the brackets and the operator are written unquoted and unescaped — the subscript's CONTENT and the
|
|
150
|
+
* value may be quoted and still leave the word an assignment. */
|
|
151
|
+
readonly bare: boolean;
|
|
152
|
+
readonly form: "scalar" | "compound";
|
|
153
|
+
/** `op === "="` with no subscript and a scalar value: the whole name is settled by this word's text. */
|
|
154
|
+
readonly settlesWholeName: boolean;
|
|
155
|
+
}
|
|
156
|
+
/** An argv word. `raw` is what the SHELL sees (quotes, escapes and substitution text retained); `folded` is what
|
|
157
|
+
* the PROGRAM receives after quote removal, with every substitution replaced by {@link SUBSTITUTION_PLACEHOLDER},
|
|
158
|
+
* every other expansion left in its written spelling, and a literal leading tilde spelled `./~…`
|
|
159
|
+
* ({@link literalTildeSpelling}). Both are views of {@link LexedWord.pieces}. */
|
|
160
|
+
export interface LexedWord {
|
|
161
|
+
readonly kind: "word";
|
|
162
|
+
readonly raw: string;
|
|
163
|
+
readonly folded: string;
|
|
164
|
+
readonly pieces: readonly WordPiece[];
|
|
165
|
+
readonly quotedSpans: readonly QuotedSpan[];
|
|
166
|
+
/** The pieces that carry an expansion, in order. */
|
|
167
|
+
readonly expansions: readonly WordPiece[];
|
|
168
|
+
readonly assignment?: AssignmentFact;
|
|
169
|
+
/** The first reason, in reading order, that a value in this word cannot be read off the text. */
|
|
170
|
+
readonly unresolved?: UnresolvedReason;
|
|
171
|
+
/** The word was read where bash accepts an assignment or a command name: at a segment's start, after a bare
|
|
172
|
+
* assignment word, after a bare control-prefix word, or after a leading redirection list. */
|
|
173
|
+
readonly atCommandPosition: boolean;
|
|
174
|
+
}
|
|
175
|
+
/** A redirection and the word it takes. A descriptor duplication (`>&2`, `<&0`, `>&-`, `>&1-`) takes none. */
|
|
176
|
+
export interface LexedRedirect {
|
|
177
|
+
readonly kind: "redirect";
|
|
178
|
+
readonly op: "<" | ">" | ">>" | "<<<" | "&>" | "&>>" | ">&" | "<&";
|
|
179
|
+
readonly target?: LexedWord;
|
|
180
|
+
}
|
|
181
|
+
/** A here-document: the body is DATA the program reads on stdin, never command text. `quoted` records whether
|
|
182
|
+
* the delimiter word carried a quote or an escape, which is bash's rule for "the body does not expand". */
|
|
183
|
+
export interface LexedHeredoc {
|
|
184
|
+
readonly kind: "heredoc";
|
|
185
|
+
readonly delimiter: string;
|
|
186
|
+
readonly quoted: boolean;
|
|
187
|
+
readonly body: string;
|
|
188
|
+
}
|
|
189
|
+
/** A subshell parenthesis, standing alone: structure, not a word. */
|
|
190
|
+
export interface LexedGroup {
|
|
191
|
+
readonly kind: "group";
|
|
192
|
+
readonly op: "(" | ")";
|
|
193
|
+
}
|
|
194
|
+
export type LexedToken = LexedWord | LexedRedirect | LexedHeredoc | LexedGroup;
|
|
195
|
+
/** The connector bash ran a segment through — `"start"` for the first one. A closed set, because the read
|
|
196
|
+
* boundary distinguishes all four: `&&` is the only one under which the previous segment is known to have
|
|
197
|
+
* exited ZERO, `||` is the only one under which the previous segment may have been SKIPPED ENTIRELY, `|`
|
|
198
|
+
* puts the segment in a subshell whose directory changes never reach the parent, and `;` is plain sequence. */
|
|
199
|
+
export type ShellSegmentConnector = "start" | ";" | "&&" | "||" | "|";
|
|
200
|
+
/** The lenient mode's two extra boundaries: a backgrounding `&` and a line break. */
|
|
201
|
+
export type LenientConnector = ShellSegmentConnector | "&" | "\n";
|
|
202
|
+
export interface LexedSegment {
|
|
203
|
+
/** The segment's text exactly as written (surrounding whitespace included). Here-document bodies are not part
|
|
204
|
+
* of any segment's text. */
|
|
205
|
+
readonly text: string;
|
|
206
|
+
readonly connector: LenientConnector;
|
|
207
|
+
readonly tokens: readonly LexedToken[];
|
|
208
|
+
/** The bodies of the substitutions found in THIS segment (its words and its redirection targets), in order. */
|
|
209
|
+
readonly substitutions: readonly string[];
|
|
210
|
+
}
|
|
211
|
+
export interface LexedCommand {
|
|
212
|
+
readonly segments: readonly LexedSegment[];
|
|
213
|
+
/** Every substitution body of the command, in order (the per-segment lists concatenated). */
|
|
214
|
+
readonly substitutions: readonly string[];
|
|
215
|
+
}
|
|
216
|
+
export interface LexParseFailure {
|
|
217
|
+
readonly parseFailure: {
|
|
218
|
+
readonly reason: ParseFailureReason;
|
|
219
|
+
readonly at: number;
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/** A command: its segments, or the one reason it has none. */
|
|
223
|
+
export type ShellLex = LexedCommand | LexParseFailure;
|
|
224
|
+
/** One segment's argv words in both spellings, index-aligned by construction (folding is strictly per word — it
|
|
225
|
+
* never splits or joins). The one shape every downstream arm of the classifier reads. */
|
|
226
|
+
export interface SegmentTokens {
|
|
227
|
+
readonly folded: readonly string[];
|
|
228
|
+
readonly raw: readonly string[];
|
|
229
|
+
}
|
|
230
|
+
/** What a substitution leaves in a word's FOLDED spelling: a marker no argv word can spell, so a consumer that
|
|
231
|
+
* judges words sees "this word carries text the shell computes" rather than the computing text itself. */
|
|
232
|
+
export declare const SUBSTITUTION_PLACEHOLDER = " subst";
|
|
233
|
+
/** Shell control words that stand BEFORE a command: transparent for the boundary walk (the command behind them is
|
|
234
|
+
* judged), and a position after which bash still accepts an assignment word. */
|
|
235
|
+
export declare const SHELL_CONTROL_PREFIXES: ReadonlySet<string>;
|
|
236
|
+
export type QuoteOpen = '"' | "'" | undefined;
|
|
237
|
+
/**
|
|
238
|
+
* Does the `$` at `at` OPEN a quote form (`$'…'` / `$"…"`)? ONE question, asked by every walker that has to
|
|
239
|
+
* answer it. Escaped means an ODD run of backslashes in front of it: `\$'y'` is a literal `$`, while `x\\$'y'`
|
|
240
|
+
* is a literal backslash followed by a real ANSI-C quote. Counting the run is what a one-character peek behind
|
|
241
|
+
* cannot do.
|
|
242
|
+
*/
|
|
243
|
+
export declare function dollarOpensQuote(text: string, at: number): boolean;
|
|
244
|
+
/** The `'` that CLOSES the ANSI-C quote opened at `open`, or `undefined` when none does. `\'` does not close
|
|
245
|
+
* one — the single place a quote region honours a backslash. */
|
|
246
|
+
export declare function ansiCQuoteClose(source: string, open: number): number | undefined;
|
|
247
|
+
/**
|
|
248
|
+
* Which QUOTE holds each character of `s`, by the same pairing rule the lexer walks with, and the boolean view of
|
|
249
|
+
* it. The pairing quote characters belong to the region they bound. `balanced` is false when a region is still
|
|
250
|
+
* open at the end of the string; callers must then NOT use the mask. This models bash only for the residual
|
|
251
|
+
* character space the strict faces operate in, where backslash escapes are rejected outright.
|
|
252
|
+
*/
|
|
253
|
+
export declare function quoteMask(s: string): {
|
|
254
|
+
openedBy: readonly QuoteOpen[];
|
|
255
|
+
quoted: readonly boolean[];
|
|
256
|
+
balanced: boolean;
|
|
257
|
+
};
|
|
258
|
+
/** The ONE spelling of a LITERAL leading tilde — `./~…`, the relative path it is — for every position bash does not
|
|
259
|
+
* expand it in; a word without a leading tilde is returned as is. */
|
|
260
|
+
export declare function literalTildeSpelling(word: string): string;
|
|
261
|
+
/** The argv-word projection of a segment: redirections, here-documents and groups are not words the program
|
|
262
|
+
* receives (bash removes them before exec). */
|
|
263
|
+
export declare function wordsOf(segment: LexedSegment): SegmentTokens;
|
|
264
|
+
/** The substitution bodies of an UNQUOTED here-document body: `$(…)` and backticks are live there, quotes are
|
|
265
|
+
* ordinary characters, a backslash protects the next character. `undefined` when a substitution never closes. */
|
|
266
|
+
export declare function substitutionsInHeredocBody(body: string): string[] | undefined;
|
|
267
|
+
/** Lex a command: its segments, or the one reason bash (or this lexer) cannot delimit them. */
|
|
268
|
+
export declare function lexShell(source: string, opts: LexOptions): ShellLex;
|
|
269
|
+
/**
|
|
270
|
+
* The refused-reading view of {@link lexShell}: a parse failure, and any word whose value cannot be read off the
|
|
271
|
+
* text and whose reason {@link UNRESOLVED_VIEW} refuses, become the one `{ reject }` sentence a consumer's
|
|
272
|
+
* unreadable arm already takes.
|
|
273
|
+
*/
|
|
274
|
+
export declare function lexShellText(source: string, opts: LexOptions): LexedCommand | {
|
|
275
|
+
reject: string;
|
|
276
|
+
};
|
|
277
|
+
/** The constructs of this dialect the command leans on — what a POSIX `sh` would read as something else. Empty
|
|
278
|
+
* when the command reads the same under either shell's word grammar. */
|
|
279
|
+
export declare function bashOnlyConstructsOf(command: LexedCommand): readonly BashOnlyConstruct[];
|