@sema-agent/core 5.38.0 → 5.40.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 +182 -10
- package/dist/agents/send-message-tool.d.ts +8 -0
- package/dist/agents/send-message-tool.js +8 -0
- package/dist/agents/teacher.js +9 -3
- package/dist/agents/verify.js +9 -3
- package/dist/core/checkpoint-store.d.ts +12 -0
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +23 -0
- package/dist/core/hooks.js +53 -4
- package/dist/core/mailbox-store.d.ts +39 -0
- package/dist/core/mailbox-store.js +9 -0
- package/dist/core/memory-engine/engine.d.ts +27 -0
- package/dist/core/memory-engine/engine.js +103 -1
- package/dist/core/memory-engine/export-bundle.d.ts +192 -0
- package/dist/core/memory-engine/export-bundle.js +306 -0
- package/dist/core/memory-engine/file-backend.d.ts +178 -1
- package/dist/core/memory-engine/file-backend.js +637 -6
- package/dist/core/memory-engine/index.d.ts +2 -1
- package/dist/core/memory-engine/index.js +1 -0
- package/dist/core/memory-engine/layout.d.ts +89 -1
- package/dist/core/memory-engine/layout.js +131 -1
- package/dist/core/memory-engine/memory-backend-contract.d.ts +1 -1
- package/dist/core/memory-engine/memory-backend-contract.js +52 -0
- package/dist/core/memory-engine/tools.js +8 -1
- package/dist/core/permission-rule-consent.d.ts +27 -4
- package/dist/core/permission-rule-consent.js +41 -4
- package/dist/core/permission-rule-model.d.ts +7 -1
- package/dist/core/runner/prepare-task.js +24 -3
- package/dist/core/runner/runtask.js +5 -0
- package/dist/core/runner/synthetic-tools.js +3 -1
- package/dist/core/runner/tool-disclosure.js +2 -1
- package/dist/core/sensitive-path-policy.js +3 -3
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +29 -1
- package/dist/core/store-contracts/mailbox-store-contract.js +78 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/write-protect.d.ts +73 -0
- package/dist/core/write-protect.js +195 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/orchestration/governance-baseline-validity.d.ts +44 -0
- package/dist/orchestration/governance-baseline-validity.js +55 -0
- package/dist/orchestration/run-workflow-tool.js +33 -8
- package/dist/orchestration/workflow-script-runner.js +9 -4
- package/dist/tools/fs/read-deny.d.ts +15 -5
- package/dist/tools/fs/read-deny.js +33 -12
- package/dist/tools/fs/safety.d.ts +5 -2
- package/dist/tools/fs/safety.js +5 -3
- package/dist/tools/fs/search.d.ts +33 -0
- package/dist/tools/fs/search.js +72 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +21 -1
package/dist/index.js
CHANGED
|
@@ -71,6 +71,7 @@ export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, }
|
|
|
71
71
|
export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
|
|
72
72
|
export { READ_FACE_DEFAULT_DENY_ENTRIES, READ_FACE_BUILTIN_DENY_TABLE, READ_DENY_BUILTIN_TIERS, READ_DENY_DEFAULT_TIERS, resolveReadDenyBuiltins, compileReadDeny, } from "./tools/fs/index.js";
|
|
73
73
|
export { deploymentReadFaceClampNotice, resolveReadFace } from "./tools/fs/index.js";
|
|
74
|
+
export { WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, compileWriteProtection, } from "./core/write-protect.js";
|
|
74
75
|
export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, toolResultContentSegment, MAX_MINTED_TOOL_RESULT_REF_CHARS, assertToolResultProvenanceMatch, normalizeToolResultProvenance, toolResultProvenanceOf, ToolResultRefConflictError, TOOL_RESULT_REF_CONFLICT_CODE, } from "./core/tool-result-store.js";
|
|
75
76
|
export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
|
|
76
77
|
export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, } from "./core/usage-window-store.js";
|
|
@@ -93,7 +94,7 @@ export {} from "./core/checkpoint-store.js";
|
|
|
93
94
|
export {} from "./core/checkpoint-store.js";
|
|
94
95
|
export { projectHumanInput, buildHumanInputEvent, } from "./core/human-input-projection.js";
|
|
95
96
|
export { TaskRegistry, defaultTaskRegistry, createTaskOutputTool, createTaskStopTool, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccessWorkflowRun, DURABLE_AGENT_HANDLE_RE, } from "./core/task-registry.js";
|
|
96
|
-
export { InMemoryMailboxStore } from "./core/mailbox-store.js";
|
|
97
|
+
export { InMemoryMailboxStore, MailboxStoreError, MAILBOX_TOMBSTONED_RECIPIENT_CODE, } from "./core/mailbox-store.js";
|
|
97
98
|
export { FileMailboxStore } from "./stores/file/mailbox-store.js";
|
|
98
99
|
export { createFileTaskListStore } from "./stores/file/task-list-store.js";
|
|
99
100
|
export { createCcFileTaskListStore } from "./stores/cc/task-list-store.js";
|
|
@@ -127,7 +128,7 @@ export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootA
|
|
|
127
128
|
export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdoptionQuarantine, readAdoptionStatus, } from "./stores/file/adoption/adopt.js";
|
|
128
129
|
export { formatHookFeedback, runToolGate, createHookEnvCapabilities, createPreToolUseConstraintPolicy, normalizePersistedRuleHit, } from "./core/hooks.js";
|
|
129
130
|
export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, } from "./core/memory.js";
|
|
130
|
-
export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, readV2HeaderHints, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, erasureSelectHash, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
|
|
131
|
+
export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, readV2HeaderHints, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, erasureSelectHash, computeMemoryBundleHash, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
|
|
131
132
|
export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, } from "./core/shared-memory/types.js";
|
|
132
133
|
export { sharedMemoryStoreContract, } from "./core/shared-memory/contract.js";
|
|
133
134
|
export { termSet, jaccardDistance, cosineDistance } from "./core/memory-vector.js";
|
|
@@ -178,7 +179,7 @@ export { sessionRepoContract } from "./core/store-contracts/session-repo-contrac
|
|
|
178
179
|
export { toolResultStoreContract } from "./core/store-contracts/tool-result-store-contract.js";
|
|
179
180
|
export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
|
|
180
181
|
export { permissionRuleSyncContract } from "./core/store-contracts/permission-rule-sync-contract.js";
|
|
181
|
-
export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
|
|
182
|
+
export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, mailboxTombstonedRecipientContract, } from "./core/store-contracts/mailbox-store-contract.js";
|
|
182
183
|
export { BACKGROUND_AGENT_CONTRACT_SCOPE, backgroundAgentStoreContract, backgroundAgentStoreScopesContract, } from "./core/store-contracts/background-agent-store-contract.js";
|
|
183
184
|
export { canAccessAgentRecord, STALE_RUNNING_REAP_ATTRIBUTION, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, } from "./core/background-agent-store.js";
|
|
184
185
|
export { serveDurableAgentRowLane, buildAgentPollDetails, } from "./core/task-registry-agent.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/98 §6.3 — the ONE reading of "is this governance baseline usable", shared by the deployment
|
|
3
|
+
* CAPABILITY predicate (`workflowsCapability`, which drives the surfaced `/workflows` affordance AND
|
|
4
|
+
* the orchestration-prompt injection) and the MOUNT-time validator (`createRunWorkflowTool`, which
|
|
5
|
+
* refuses loudly with a code).
|
|
6
|
+
*
|
|
7
|
+
* Why one function rather than two agreeing predicates: this seam split three times inside one
|
|
8
|
+
* release window, each time one level deeper. First the capability tested `!== undefined` while the
|
|
9
|
+
* mount tested truthiness (a JSON `null` announced workflows the tool never mounted). Then both
|
|
10
|
+
* tested the container while the mount refused an unusable `base` SLOT (`{ base: null }` announced
|
|
11
|
+
* workflows that hard-failed every opted-in task). Then the slot readings agreed but the mount
|
|
12
|
+
* additionally refused a malformed `worktreeBase` and malformed face lists — the same split again,
|
|
13
|
+
* one field further in. Two predicates that must "stay in agreement" are a standing invitation to
|
|
14
|
+
* drift; the only shape that cannot drift is a single function both call.
|
|
15
|
+
*
|
|
16
|
+
* The contract: {@link governanceBaselineProblem} returns `null` when the mount would SUCCEED, and
|
|
17
|
+
* a describable problem when the mount would REFUSE. Every mount-time refusal must be reachable
|
|
18
|
+
* from here, so a deployment is never told it has workflows it cannot actually run.
|
|
19
|
+
*/
|
|
20
|
+
export interface GovernanceBaselineProblem {
|
|
21
|
+
/** Which slot/field is wrong, in the spelling a deployment configures (`base.excludeTools`). */
|
|
22
|
+
readonly where: string;
|
|
23
|
+
/** What was found there, for the operator-facing message (`null`, `a string`, `an array`). */
|
|
24
|
+
readonly found: string;
|
|
25
|
+
/** The coded refusal the mount raises for this problem. */
|
|
26
|
+
readonly code: "config.invalid_governance_baseline" | "config.invalid_tool_name_set";
|
|
27
|
+
}
|
|
28
|
+
/** Name a bad value WITHOUT throwing on it (a bigint / cyclic object / poisoned `toJSON` must not
|
|
29
|
+
* crash the describer and cost the refusal its name). */
|
|
30
|
+
export declare function describeBaselineValue(v: unknown): string;
|
|
31
|
+
/**
|
|
32
|
+
* The single validity reading. `null` ⇒ the mount would succeed ⇒ the capability may be announced.
|
|
33
|
+
*
|
|
34
|
+
* `base` is the control plane every script-spawned child inherits, so a non-object there refuses:
|
|
35
|
+
* ungoverned is spelled by not configuring `workflowGovernanceBaseline` at all, never by an
|
|
36
|
+
* unusable anchor. `worktreeBase` is the optional overlay whose documented absence means "base
|
|
37
|
+
* governs every child" — `null`/absent is that absence, any other non-object is garbage and refuses
|
|
38
|
+
* rather than impersonating either absence or an overlay.
|
|
39
|
+
*/
|
|
40
|
+
export declare function governanceBaselineProblem(baseline: unknown): GovernanceBaselineProblem | null;
|
|
41
|
+
/** The mount's refusal, minted from the shared reading so the message and the code have one home. */
|
|
42
|
+
export declare function governanceBaselineError(p: GovernanceBaselineProblem): Error & {
|
|
43
|
+
code: string;
|
|
44
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const FACE_LIST_FIELDS = ["excludeTools", "deferTools", "alwaysLoadTools"];
|
|
2
|
+
export function describeBaselineValue(v) {
|
|
3
|
+
return v === null ? "null" : Array.isArray(v) ? "an array" : `a ${typeof v}`;
|
|
4
|
+
}
|
|
5
|
+
function isSpecObject(v) {
|
|
6
|
+
return v !== null && v !== undefined && typeof v === "object" && !Array.isArray(v);
|
|
7
|
+
}
|
|
8
|
+
function faceListProblem(slot, where) {
|
|
9
|
+
for (const field of FACE_LIST_FIELDS) {
|
|
10
|
+
const v = slot[field];
|
|
11
|
+
if (v === undefined || v === null)
|
|
12
|
+
continue;
|
|
13
|
+
if (!Array.isArray(v)) {
|
|
14
|
+
return { where: `${where}.${field}`, found: describeBaselineValue(v), code: "config.invalid_tool_name_set" };
|
|
15
|
+
}
|
|
16
|
+
for (const [i, entry] of v.entries()) {
|
|
17
|
+
if (typeof entry !== "string") {
|
|
18
|
+
return {
|
|
19
|
+
where: `${where}.${field}`,
|
|
20
|
+
found: `entry ${i} is ${describeBaselineValue(entry)}`,
|
|
21
|
+
code: "config.invalid_tool_name_set",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export function governanceBaselineProblem(baseline) {
|
|
29
|
+
if (!isSpecObject(baseline)) {
|
|
30
|
+
return { where: "workflowGovernanceBaseline", found: describeBaselineValue(baseline), code: "config.invalid_governance_baseline" };
|
|
31
|
+
}
|
|
32
|
+
const base = baseline.base;
|
|
33
|
+
if (!isSpecObject(base)) {
|
|
34
|
+
return { where: "base", found: describeBaselineValue(base), code: "config.invalid_governance_baseline" };
|
|
35
|
+
}
|
|
36
|
+
const baseFaces = faceListProblem(base, "base");
|
|
37
|
+
if (baseFaces !== null)
|
|
38
|
+
return baseFaces;
|
|
39
|
+
const wt = baseline.worktreeBase;
|
|
40
|
+
if (wt === null || wt === undefined)
|
|
41
|
+
return null;
|
|
42
|
+
if (!isSpecObject(wt)) {
|
|
43
|
+
return { where: "worktreeBase", found: describeBaselineValue(wt), code: "config.invalid_governance_baseline" };
|
|
44
|
+
}
|
|
45
|
+
return faceListProblem(wt, "worktreeBase");
|
|
46
|
+
}
|
|
47
|
+
export function governanceBaselineError(p) {
|
|
48
|
+
const detail = p.code === "config.invalid_tool_name_set"
|
|
49
|
+
? `must be an array of tool names (got ${p.found}) — refusing to mount rather than govern children with a roster nobody can read.`
|
|
50
|
+
: `is ${p.found}, not a spec object — refusing to mount rather than launch children under no governance ` +
|
|
51
|
+
`(to run ungoverned, leave workflowGovernanceBaseline unconfigured).`;
|
|
52
|
+
const e = new Error(`createRunWorkflowTool: the workflow governance baseline's ${p.where} ${detail}`);
|
|
53
|
+
e.code = p.code;
|
|
54
|
+
return e;
|
|
55
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool, errorResult } from "../core/tools.js";
|
|
3
|
+
import { governanceBaselineError, governanceBaselineProblem } from "./governance-baseline-validity.js";
|
|
3
4
|
import { redactSecrets, redactHostLeaks, boundedRedactedSummary } from "../core/untrusted-egress.js";
|
|
4
5
|
import { startWorkflow } from "./workflow.js";
|
|
5
6
|
import { buildWorkflowPrimitives } from "./workflow-primitives.js";
|
|
@@ -167,16 +168,40 @@ export async function createRunWorkflowTool(d) {
|
|
|
167
168
|
childMaxTokens: lim.childMaxTokens,
|
|
168
169
|
childMaxTurns: lim.childMaxTurns,
|
|
169
170
|
};
|
|
171
|
+
const problem = governanceBaselineProblem(d.governanceBaseline);
|
|
172
|
+
if (problem !== null)
|
|
173
|
+
throw governanceBaselineError(problem);
|
|
174
|
+
const FACE_LIST_FIELDS = ["excludeTools", "deferTools", "alwaysLoadTools"];
|
|
175
|
+
const dropNullFaces = (slot) => {
|
|
176
|
+
let out = slot;
|
|
177
|
+
for (const field of FACE_LIST_FIELDS) {
|
|
178
|
+
if (slot[field] !== null)
|
|
179
|
+
continue;
|
|
180
|
+
out = { ...out };
|
|
181
|
+
delete out[field];
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
};
|
|
185
|
+
const sanitizedBaseline = (() => {
|
|
186
|
+
const wt = d.governanceBaseline.worktreeBase;
|
|
187
|
+
const { worktreeBase: _absentOverlay, ...rest } = d.governanceBaseline;
|
|
188
|
+
return {
|
|
189
|
+
...rest,
|
|
190
|
+
base: dropNullFaces(d.governanceBaseline.base),
|
|
191
|
+
...(wt === null || wt === undefined ? {} : { worktreeBase: dropNullFaces(wt) }),
|
|
192
|
+
};
|
|
193
|
+
})();
|
|
194
|
+
const unionFaceList = (own, parent) => own === undefined ? [...new Set(parent)] : [...new Set([...own, ...parent])];
|
|
170
195
|
const withParentFace = (base, inherit) => ({
|
|
171
196
|
...base,
|
|
172
197
|
...(d.parentExcludeTools?.length
|
|
173
|
-
? { excludeTools:
|
|
198
|
+
? { excludeTools: unionFaceList(base.excludeTools ?? inherit?.excludeTools, d.parentExcludeTools) }
|
|
174
199
|
: {}),
|
|
175
200
|
...(d.parentDeferTools?.length
|
|
176
|
-
? { deferTools:
|
|
201
|
+
? { deferTools: unionFaceList(base.deferTools ?? inherit?.deferTools, d.parentDeferTools) }
|
|
177
202
|
: {}),
|
|
178
203
|
...(d.parentAlwaysLoadTools?.length
|
|
179
|
-
? { alwaysLoadTools:
|
|
204
|
+
? { alwaysLoadTools: unionFaceList(base.alwaysLoadTools ?? inherit?.alwaysLoadTools, d.parentAlwaysLoadTools) }
|
|
180
205
|
: {}),
|
|
181
206
|
...(() => {
|
|
182
207
|
const parent = d.parentRestoreGatedTools;
|
|
@@ -197,13 +222,13 @@ export async function createRunWorkflowTool(d) {
|
|
|
197
222
|
const withParentProfile = (base) => d.parentPromptProfile !== undefined && base.promptProfile === undefined ? { ...base, promptProfile: d.parentPromptProfile } : base;
|
|
198
223
|
const baselineWithParentFace = (d.parentExcludeTools?.length ?? 0) > 0 || (d.parentDeferTools?.length ?? 0) > 0 || (d.parentAlwaysLoadTools?.length ?? 0) > 0 || d.parentPromptProfile !== undefined || d.parentRestoreGatedTools !== undefined
|
|
199
224
|
? {
|
|
200
|
-
...
|
|
201
|
-
base: withParentProfile(withParentFace(
|
|
202
|
-
...(
|
|
203
|
-
? { worktreeBase: withParentFace(
|
|
225
|
+
...sanitizedBaseline,
|
|
226
|
+
base: withParentProfile(withParentFace(sanitizedBaseline.base)),
|
|
227
|
+
...(sanitizedBaseline.worktreeBase !== undefined
|
|
228
|
+
? { worktreeBase: withParentFace(sanitizedBaseline.worktreeBase, sanitizedBaseline.base) }
|
|
204
229
|
: {}),
|
|
205
230
|
}
|
|
206
|
-
:
|
|
231
|
+
: sanitizedBaseline;
|
|
207
232
|
const governance = { baseline: baselineWithParentFace, models: d.models, caps: childCaps, onNotice: d.onNotice };
|
|
208
233
|
const builtinsEnabled = d.builtinWorkflows !== false;
|
|
209
234
|
const namedWorkflowSection = renderNamedWorkflowListing(await collectNamedWorkflowListings(d.scriptStore, builtinsEnabled));
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { governanceBaselineProblem } from "./governance-baseline-validity.js";
|
|
1
2
|
export function isSelfOrchestrationActive(spec, deps) {
|
|
2
3
|
return spec.selfOrchestration === true && workflowsCapability(deps);
|
|
3
4
|
}
|
|
4
5
|
export function workflowsCapability(deps) {
|
|
5
|
-
return
|
|
6
|
-
|
|
6
|
+
return deps.workflowScriptRunner?.safeForUntrustedScripts === true && hasUsableGovernanceBaseline(deps);
|
|
7
|
+
}
|
|
8
|
+
function hasUsableGovernanceBaseline(deps) {
|
|
9
|
+
return governanceBaselineProblem(deps.workflowGovernanceBaseline) === null;
|
|
7
10
|
}
|
|
8
11
|
export function selfOrchestrationFailClosedReason(spec, deps) {
|
|
9
12
|
if (spec.selfOrchestration !== true)
|
|
@@ -14,8 +17,10 @@ export function selfOrchestrationFailClosedReason(spec, deps) {
|
|
|
14
17
|
if (deps.workflowScriptRunner?.safeForUntrustedScripts !== true) {
|
|
15
18
|
missing.push("a hard WorkflowScriptRunner (RunnerDeps.workflowScriptRunner.safeForUntrustedScripts === true)");
|
|
16
19
|
}
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
const problem = governanceBaselineProblem(deps.workflowGovernanceBaseline);
|
|
21
|
+
if (problem !== null) {
|
|
22
|
+
missing.push(`a usable governance baseline (RunnerDeps.workflowGovernanceBaseline.${problem.where === "workflowGovernanceBaseline" ? "" : `${problem.where} `}` +
|
|
23
|
+
`is ${problem.found})`);
|
|
19
24
|
}
|
|
20
25
|
return (`self-orchestration (TaskSpec.selfOrchestration) requires ${missing.join(" and ")}; it is DISABLED for ` +
|
|
21
26
|
`this task (fail-closed — the run_workflow tool is not mounted and the orchestration prompt is not injected).`);
|
|
@@ -40,10 +40,18 @@ export interface ReadDenyMatcher {
|
|
|
40
40
|
/** Normalized entries (built-ins first, then additions; exact duplicates folded). The disclosure /
|
|
41
41
|
* persistence face. */
|
|
42
42
|
readonly entries: readonly NormalizedReadDenyEntry[];
|
|
43
|
-
/** Judge ONE path view (segment-window semantics). Returns the first matching entry, or null.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
/** Judge ONE path view (segment-window semantics). Returns the first matching entry, or null.
|
|
44
|
+
* `aliasResolved` = the key's MINTER reports the real filesystem already collapsed any win32
|
|
45
|
+
* component alias in it (canonicalizeTarget's realpath arm); unset ⇒ both readings are judged,
|
|
46
|
+
* which is the fail-closed default. */
|
|
47
|
+
matchPath(path: string, opts?: {
|
|
48
|
+
readonly aliasResolved?: boolean;
|
|
49
|
+
}): ReadDenyHit | null;
|
|
50
|
+
/** §3.3 target judgment: canonical view + lexical view; EITHER hit decides. `aliasResolved`
|
|
51
|
+
* applies to the CANONICAL arm only — the lexical view is an unresolved spelling. */
|
|
52
|
+
matchTarget(canonicalKey: string, lexicalView?: string, opts?: {
|
|
53
|
+
readonly aliasResolved?: boolean;
|
|
54
|
+
}): ReadDenyHit | null;
|
|
47
55
|
/** Traversal exclusions for the rg legs: `!`-polarity glob pairs — `!**\/<p>` plus `!**\/<p>/…`
|
|
48
56
|
* (the entry itself and its descendants). */
|
|
49
57
|
readonly rgExclusionGlobs: readonly ReadDenyRgGlob[];
|
|
@@ -145,7 +153,9 @@ export declare function compileSegmentPattern(raw: string, fold: "none" | "ascii
|
|
|
145
153
|
* BOTH separator families — win32 canonical keys are backslash-form. Returns the matched raw pattern
|
|
146
154
|
* or null.
|
|
147
155
|
*/
|
|
148
|
-
export declare function matchSegmentPatterns(path: string, compiled: readonly CompiledSegmentPattern[]
|
|
156
|
+
export declare function matchSegmentPatterns(path: string, compiled: readonly CompiledSegmentPattern[], opts?: {
|
|
157
|
+
readonly aliasResolved?: boolean;
|
|
158
|
+
}): string | null;
|
|
149
159
|
/**
|
|
150
160
|
* codex r3 — the PURE validation/canonicality predicate over a PERSISTED deny entry (checkpoint v9
|
|
151
161
|
* face section), for the resume pre-CAS ladder: it must refuse BEFORE the approval CAS everything
|
|
@@ -91,15 +91,36 @@ export function compileSegmentPattern(raw, fold) {
|
|
|
91
91
|
});
|
|
92
92
|
return { raw, segments, foldAscii: fold === "ascii" };
|
|
93
93
|
}
|
|
94
|
-
export function matchSegmentPatterns(path, compiled) {
|
|
95
|
-
const
|
|
96
|
-
|
|
94
|
+
export function matchSegmentPatterns(path, compiled, opts) {
|
|
95
|
+
const literalSegs = path.split(/[\\/]/).filter(Boolean);
|
|
96
|
+
const win32Segs = [];
|
|
97
|
+
for (const raw of literalSegs) {
|
|
98
|
+
const dot = /^\.{1,2}$/.test(raw) ? raw : raw.replace(/ +$/, "");
|
|
99
|
+
if (dot === ".")
|
|
100
|
+
continue;
|
|
101
|
+
if (dot === "..") {
|
|
102
|
+
const last = win32Segs[win32Segs.length - 1];
|
|
103
|
+
if (win32Segs.length > 0 && last !== "..")
|
|
104
|
+
win32Segs.pop();
|
|
105
|
+
else
|
|
106
|
+
win32Segs.push("..");
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const stripped = raw.replace(/[. ]+$/, "");
|
|
110
|
+
win32Segs.push(stripped.length > 0 ? stripped : raw);
|
|
111
|
+
}
|
|
112
|
+
const sameView = opts?.aliasResolved === true ||
|
|
113
|
+
(win32Segs.length === literalSegs.length && win32Segs.every((s, i) => s === literalSegs[i]));
|
|
114
|
+
const views = sameView ? [literalSegs] : [literalSegs, win32Segs];
|
|
115
|
+
const lowered = new Map();
|
|
97
116
|
for (const pat of compiled) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
for (const view of views) {
|
|
118
|
+
const segs = pat.foldAscii ? (lowered.get(view) ?? lowered.set(view, view.map(asciiLower)).get(view)) : view;
|
|
119
|
+
const n = pat.segments.length;
|
|
120
|
+
for (let i = 0; i + n <= segs.length; i++) {
|
|
121
|
+
if (pat.segments.every((re, j) => re.test(segs[i + j] ?? "")))
|
|
122
|
+
return pat.raw;
|
|
123
|
+
}
|
|
103
124
|
}
|
|
104
125
|
}
|
|
105
126
|
return null;
|
|
@@ -171,15 +192,15 @@ export function compileReadDeny(additions = [], layer = "additions", builtin) {
|
|
|
171
192
|
rgExclusionGlobs.push({ flag, glob: `!**/${g}` }, { flag, glob: `!**/${g}/**` });
|
|
172
193
|
rgProbeGlobs.push({ flag, glob: `**/${g}` }, { flag, glob: `**/${g}/**` });
|
|
173
194
|
}
|
|
174
|
-
const matchPath = (path) => {
|
|
175
|
-
const hit = matchSegmentPatterns(path, compiled);
|
|
195
|
+
const matchPath = (path, opts) => {
|
|
196
|
+
const hit = matchSegmentPatterns(path, compiled, opts);
|
|
176
197
|
return hit === null ? null : { pattern: hit };
|
|
177
198
|
};
|
|
178
199
|
return {
|
|
179
200
|
entries: normalized,
|
|
180
201
|
matchPath,
|
|
181
|
-
matchTarget(canonicalKey, lexicalView) {
|
|
182
|
-
const canonical = matchPath(canonicalKey);
|
|
202
|
+
matchTarget(canonicalKey, lexicalView, opts) {
|
|
203
|
+
const canonical = matchPath(canonicalKey, opts);
|
|
183
204
|
if (canonical !== null)
|
|
184
205
|
return { ...canonical, matchedView: canonicalKey };
|
|
185
206
|
if (lexicalView !== undefined) {
|
|
@@ -268,7 +268,9 @@ export declare function lexicalViewOf(spelled: string, base: string): string;
|
|
|
268
268
|
* `executionEnv` — see design/44 §5.)
|
|
269
269
|
*/
|
|
270
270
|
export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRootsCanonical?: readonly string[], exactFileReadExemption?: (canonicalKey: string) => boolean, readDeny?: {
|
|
271
|
-
matchTarget(canonicalKey: string, lexicalView?: string
|
|
271
|
+
matchTarget(canonicalKey: string, lexicalView?: string, opts?: {
|
|
272
|
+
readonly aliasResolved?: boolean;
|
|
273
|
+
}): {
|
|
272
274
|
pattern: string;
|
|
273
275
|
matchedView?: string;
|
|
274
276
|
} | null;
|
|
@@ -291,6 +293,7 @@ export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, pat
|
|
|
291
293
|
export declare function canonicalizeTarget(env: ExecutionEnv, path: string, signal?: AbortSignal, baseCwd?: string): Promise<{
|
|
292
294
|
ok: true;
|
|
293
295
|
key: string;
|
|
296
|
+
aliasResolved?: true;
|
|
294
297
|
} | {
|
|
295
298
|
ok: false;
|
|
296
299
|
message: string;
|
|
@@ -341,7 +344,7 @@ export declare function withinAnyRoot(rootsCanonical: readonly string[], p: stri
|
|
|
341
344
|
* just let the model discover it by trial), but it is a statement, not a recommendation, and it
|
|
342
345
|
* comes last. Every shell call still passes the deployment's approval policy.
|
|
343
346
|
*/
|
|
344
|
-
export declare const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories \u2014 or additionalReadDirectories for read-only access. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
|
|
347
|
+
export declare const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories \u2014 or additionalReadDirectories for read-only access; a deployment that wants reads open everywhere can declare readFace: \"open\" instead of listing directories. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
|
|
345
348
|
/** inv 1 (read-before-edit): a file must have been read this task before it can be edited/overwritten.
|
|
346
349
|
* Message is CC 2.1.198 live-verbatim (all-tools-live-probe 2026-07-08 §2.1/§3.1/§5.1 — one message for
|
|
347
350
|
* Edit/Write/NotebookEdit: "before writing to it", not the old sema "before editing").
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -339,7 +339,9 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
|
|
|
339
339
|
return { ok: false, violation: { code: "invalid", message: `path "${path}" is a blocked device/special file (reading it can hang the process); refused.` } };
|
|
340
340
|
}
|
|
341
341
|
if (readDeny !== undefined) {
|
|
342
|
-
const hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical)
|
|
342
|
+
const hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical), {
|
|
343
|
+
aliasResolved: canon.aliasResolved === true,
|
|
344
|
+
});
|
|
343
345
|
if (hit !== null) {
|
|
344
346
|
const lexicalHit = hit.matchedView !== undefined && hit.matchedView !== key;
|
|
345
347
|
const judged = lexicalHit
|
|
@@ -419,7 +421,7 @@ export async function canonicalizeTarget(env, path, signal, baseCwd) {
|
|
|
419
421
|
}
|
|
420
422
|
const canon = await env.canonicalPath(abs, signal);
|
|
421
423
|
if (canon.ok)
|
|
422
|
-
return { ok: true, key: canon.value };
|
|
424
|
+
return { ok: true, key: canon.value, aliasResolved: true };
|
|
423
425
|
const info = await env.fileInfo(abs, signal);
|
|
424
426
|
if (!info.ok) {
|
|
425
427
|
return { ok: false, message: `cannot determine whether "${path}" is a symlink: ${info.error.message}`, unresolvedSymlink: true };
|
|
@@ -491,7 +493,7 @@ export function violationDetails(v) {
|
|
|
491
493
|
export function withinAnyRoot(rootsCanonical, p) {
|
|
492
494
|
return rootsCanonical.some((r) => within(r, p));
|
|
493
495
|
}
|
|
494
|
-
export const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories — or additionalReadDirectories for read-only access. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
|
|
496
|
+
export const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories — or additionalReadDirectories for read-only access; a deployment that wants reads open everywhere can declare readFace: \"open\" instead of listing directories. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
|
|
495
497
|
export function requireRead(state, key) {
|
|
496
498
|
const entry = state.get(key);
|
|
497
499
|
if (entry === undefined || entry.isPartialView) {
|
|
@@ -208,6 +208,39 @@ export interface GrepRunResult {
|
|
|
208
208
|
* withheld / no deny judge in play. */
|
|
209
209
|
withheld?: ReadDenyWithheld;
|
|
210
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* backlog #303 (rescan-hardened form) — the rg leg's deny TRIPWIRE. rg's exclusion globs are spelled
|
|
213
|
+
* from the pattern text and cannot express the win32 component-alias family (`.aws.` / `.aws ` —
|
|
214
|
+
* trailing dots/spaces the win32 namespace strips), so a directory planted under an alias spelling
|
|
215
|
+
* slips past the globs while the shared `matchPath` (two-view, read-deny.ts) correctly names it —
|
|
216
|
+
* the JS leg pruned it, the rg leg returned its contents, and the same tree answered differently
|
|
217
|
+
* depending on whether ripgrep was installed.
|
|
218
|
+
*
|
|
219
|
+
* The first fix FILTERED rg's text output line-by-line. The rescan killed that shape twice over:
|
|
220
|
+
* ripgrep prints filenames verbatim, so a path component containing a NEWLINE splits one record
|
|
221
|
+
* across physical lines whose fragments carry no `:line:` boundary — the filter kept them and the
|
|
222
|
+
* deny-listed content passed (a protection hole, not a precision residual); and pruning every line
|
|
223
|
+
* of a partial run fabricated a `No matches.` row inside the structured card. Text-splitting rg
|
|
224
|
+
* records is unsalvageable without structured output (`--json`, backlog #306), so this function no
|
|
225
|
+
* longer edits anything: it JUDGES. Any deny hit — or any line the format cannot account for —
|
|
226
|
+
* trips, and the caller abandons the rg run for the JS scanner, whose walk prunes with the
|
|
227
|
+
* authoritative judge and needs no path parsing at all. rg stays the fast path for the common case
|
|
228
|
+
* (no guarded entries in the result); the moment a guarded spelling is involved, the engine that
|
|
229
|
+
* cannot mis-parse it owns the answer.
|
|
230
|
+
*
|
|
231
|
+
* Trip conditions by mode — `files_with_matches`: every line IS a path, judged whole; `count`:
|
|
232
|
+
* `path:N` (a line without the numeric tail is a split record — ambiguous, trip); `content`:
|
|
233
|
+
* judged at EVERY `:digits:` boundary and every `-digits-` boundary (a candidate cut inside match
|
|
234
|
+
* text can over-trip — safe: the fallback re-derives the exact answer), and a non-separator line
|
|
235
|
+
* with NO boundary at all is a split record — trip. `--` separators and empty lines pass.
|
|
236
|
+
*/
|
|
237
|
+
export declare function rgOutputDenyTripwire(stdout: string, mode: "content" | "files_with_matches" | "count", judge: Pick<ReadDenyJudge, "matchPath">): {
|
|
238
|
+
trip: false;
|
|
239
|
+
} | {
|
|
240
|
+
trip: true;
|
|
241
|
+
reason: "deny-hit" | "ambiguous-record";
|
|
242
|
+
pattern?: string;
|
|
243
|
+
};
|
|
211
244
|
/** ripgrep grep: build flags from params, run, normalize to the same output as {@link jsGrep}.
|
|
212
245
|
* Result-fidelity contract: output ripgrep DID produce is never silently replaced by a fallback
|
|
213
246
|
* rescan — partial results ship with a caveat; only a zero-output failure degrades to
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -1043,6 +1043,63 @@ function formatRgStdout(stdout, p, caveat = "") {
|
|
|
1043
1043
|
(off > 0 ? `\n[offset ${off}]` : "") +
|
|
1044
1044
|
caveat);
|
|
1045
1045
|
}
|
|
1046
|
+
export function rgOutputDenyTripwire(stdout, mode, judge) {
|
|
1047
|
+
if (stdout.length === 0)
|
|
1048
|
+
return { trip: false };
|
|
1049
|
+
const judgeBoundaries = (line, boundary) => {
|
|
1050
|
+
let sawBoundary = false;
|
|
1051
|
+
for (let m = boundary.exec(line); m !== null; m = boundary.exec(line)) {
|
|
1052
|
+
sawBoundary = true;
|
|
1053
|
+
const prefix = line.slice(0, m.index);
|
|
1054
|
+
if (prefix.length === 0)
|
|
1055
|
+
continue;
|
|
1056
|
+
const hit = judge.matchPath(prefix);
|
|
1057
|
+
if (hit !== null)
|
|
1058
|
+
return hit.pattern;
|
|
1059
|
+
}
|
|
1060
|
+
return sawBoundary ? null : "";
|
|
1061
|
+
};
|
|
1062
|
+
for (const line of stdout.split("\n")) {
|
|
1063
|
+
if (line.length === 0 || line === "--")
|
|
1064
|
+
continue;
|
|
1065
|
+
if (mode === "files_with_matches") {
|
|
1066
|
+
const h = judge.matchPath(line);
|
|
1067
|
+
if (h !== null)
|
|
1068
|
+
return { trip: true, reason: "deny-hit", pattern: h.pattern };
|
|
1069
|
+
}
|
|
1070
|
+
else if (mode === "count") {
|
|
1071
|
+
const m = /^(.*):\d+$/.exec(line);
|
|
1072
|
+
if (m === null)
|
|
1073
|
+
return { trip: true, reason: "ambiguous-record" };
|
|
1074
|
+
const h = judge.matchPath(m[1]);
|
|
1075
|
+
if (h !== null)
|
|
1076
|
+
return { trip: true, reason: "deny-hit", pattern: h.pattern };
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
const colon = judgeBoundaries(line, /:\d+:/g);
|
|
1080
|
+
if (colon !== null && colon !== "")
|
|
1081
|
+
return { trip: true, reason: "deny-hit", pattern: colon };
|
|
1082
|
+
const dash = judgeBoundaries(line, /-\d+-/g);
|
|
1083
|
+
if (dash !== null && dash !== "")
|
|
1084
|
+
return { trip: true, reason: "deny-hit", pattern: dash };
|
|
1085
|
+
if (colon === "" && dash === "")
|
|
1086
|
+
return { trip: true, reason: "ambiguous-record" };
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return { trip: false };
|
|
1090
|
+
}
|
|
1091
|
+
async function jsGrepDenyTripFallback(env, root, p, signal, trip, deny) {
|
|
1092
|
+
const denyOut = {};
|
|
1093
|
+
const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
|
|
1094
|
+
const why = trip.reason === "deny-hit" ? "its output involved sensitive-path deny-listed entries the exclusion globs cannot express" : "its output contained a record the line format cannot account for";
|
|
1095
|
+
return {
|
|
1096
|
+
text: text.startsWith("Error (grep)")
|
|
1097
|
+
? text
|
|
1098
|
+
: `${text}\n[note: the ripgrep pass was abandoned (${why}); results are from the fallback scanner, which prunes with the authoritative deny judge]`,
|
|
1099
|
+
degraded: { fallback: "js-scan", reason: `ripgrep output tripped the deny tripwire (${trip.reason})` },
|
|
1100
|
+
...(denyOut.withheld !== undefined ? { withheld: denyOut.withheld } : {}),
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1046
1103
|
async function jsGrepFallback(env, root, p, signal, reason, deny) {
|
|
1047
1104
|
const denyOut = {};
|
|
1048
1105
|
const text = await jsGrep(env, root, p, signal, undefined, deny, denyOut);
|
|
@@ -1097,6 +1154,11 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1097
1154
|
if (err.code === "timeout") {
|
|
1098
1155
|
const partial = err.partialStdout ?? "";
|
|
1099
1156
|
if (partial.trim().length > 0) {
|
|
1157
|
+
if (deny !== undefined) {
|
|
1158
|
+
const trip = rgOutputDenyTripwire(partial, mode, deny);
|
|
1159
|
+
if (trip.trip)
|
|
1160
|
+
return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
|
|
1161
|
+
}
|
|
1100
1162
|
return {
|
|
1101
1163
|
text: `${delimitUntrusted("ripgrep partial output", formatRgStdout(partial, p))}\n…[ripgrep timed out after producing partial output — results may be incomplete]`,
|
|
1102
1164
|
degraded: { partial: true, reason: "ripgrep timed out" },
|
|
@@ -1124,6 +1186,11 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1124
1186
|
}
|
|
1125
1187
|
if (exitCode >= 2) {
|
|
1126
1188
|
if (stdout.trim().length > 0) {
|
|
1189
|
+
if (deny !== undefined) {
|
|
1190
|
+
const trip = rgOutputDenyTripwire(stdout, mode, deny);
|
|
1191
|
+
if (trip.trip)
|
|
1192
|
+
return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
|
|
1193
|
+
}
|
|
1127
1194
|
return {
|
|
1128
1195
|
text: `${delimitUntrusted("ripgrep partial output", formatRgStdout(stdout, p))}\n…[ripgrep exited with an error after producing partial output — results may be incomplete]`,
|
|
1129
1196
|
degraded: { partial: true, reason: `ripgrep exited with code ${exitCode}` },
|
|
@@ -1131,6 +1198,11 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1131
1198
|
}
|
|
1132
1199
|
return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`, deny);
|
|
1133
1200
|
}
|
|
1201
|
+
if (deny !== undefined) {
|
|
1202
|
+
const trip = rgOutputDenyTripwire(stdout, mode, deny);
|
|
1203
|
+
if (trip.trip)
|
|
1204
|
+
return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
|
|
1205
|
+
}
|
|
1134
1206
|
const orderedStdout = mode === "files_with_matches" ? await sortRgFilesByMtime(env, root, stdout, signal) : stdout;
|
|
1135
1207
|
const d = await denyDisclosure();
|
|
1136
1208
|
return { text: formatRgStdout(orderedStdout, p) + d.note, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
|