@guilz-dev/belay 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -11
- package/dist/adapters/claude/runtime-entry.js +2 -0
- package/dist/adapters/codex/runtime-entry.js +2 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -1
- package/dist/adapters/cursor/hooks.js +37 -2
- package/dist/adapters/cursor/runtime-entry.js +5 -0
- package/dist/adapters/shared/gate-runtime.d.ts +6 -0
- package/dist/adapters/shared/gate-runtime.js +260 -59
- package/dist/bundle/claude-runtime.mjs +7068 -3966
- package/dist/bundle/codex-runtime.mjs +7001 -3899
- package/dist/bundle/cursor-runtime.mjs +7004 -3899
- package/dist/commands/classify-for-report.js +5 -0
- package/dist/commands/doctor.js +88 -9
- package/dist/commands/explain.js +3 -2
- package/dist/commands/metrics.js +42 -0
- package/dist/commands/recovery-checkpoints.d.ts +8 -1
- package/dist/commands/recovery-checkpoints.js +44 -11
- package/dist/commands/session.d.ts +5 -0
- package/dist/commands/session.js +8 -1
- package/dist/config-io.js +2 -2
- package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
- package/dist/conformance/contained-execution-guarantee.js +121 -0
- package/dist/conformance/guarantee-table.js +7 -0
- package/dist/core/audit-analysis.js +7 -4
- package/dist/core/audit-io.d.ts +1 -0
- package/dist/core/audit-io.js +16 -6
- package/dist/core/audit-legacy-archive.d.ts +6 -0
- package/dist/core/audit-legacy-archive.js +51 -0
- package/dist/core/audit-metrics.d.ts +15 -0
- package/dist/core/audit-metrics.js +16 -2
- package/dist/core/audit-query.d.ts +2 -0
- package/dist/core/audit-query.js +33 -9
- package/dist/core/audit-recovery-metrics.d.ts +33 -0
- package/dist/core/audit-recovery-metrics.js +146 -0
- package/dist/core/audit-serialize.d.ts +9 -0
- package/dist/core/audit-serialize.js +179 -0
- package/dist/core/audit-types.d.ts +8 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/bounded-output.d.ts +11 -0
- package/dist/core/bounded-output.js +26 -0
- package/dist/core/capability/attestation.d.ts +56 -0
- package/dist/core/capability/attestation.js +110 -3
- package/dist/core/capability/boundary-session.d.ts +5 -0
- package/dist/core/capability/boundary-session.js +38 -1
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/capability/policy-engine.js +32 -13
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +89 -10
- package/dist/core/contained-execution/docker-policy.d.ts +18 -0
- package/dist/core/contained-execution/docker-policy.js +97 -0
- package/dist/core/contained-execution/docker.d.ts +223 -0
- package/dist/core/contained-execution/docker.js +846 -0
- package/dist/core/contained-execution/eligibility.d.ts +9 -0
- package/dist/core/contained-execution/eligibility.js +103 -0
- package/dist/core/contained-execution/failure.d.ts +18 -0
- package/dist/core/contained-execution/failure.js +105 -0
- package/dist/core/contained-execution/mirror.d.ts +54 -0
- package/dist/core/contained-execution/mirror.js +587 -0
- package/dist/core/contained-execution/policy.d.ts +10 -0
- package/dist/core/contained-execution/policy.js +20 -0
- package/dist/core/decision-config-fingerprint.d.ts +3 -0
- package/dist/core/decision-config-fingerprint.js +6 -0
- package/dist/core/effect-ir/index.d.ts +1 -1
- package/dist/core/effect-ir/index.js +1 -1
- package/dist/core/effect-ir/normalize.d.ts +5 -0
- package/dist/core/effect-ir/normalize.js +17 -7
- package/dist/core/effect-ir/shell-build.js +2 -14
- package/dist/core/effect-ir/shell-lower.js +103 -25
- package/dist/core/gate-contract.d.ts +4 -1
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/process-runner.d.ts +10 -3
- package/dist/core/process-runner.js +66 -10
- package/dist/core/recovery/checkpoint.d.ts +3 -0
- package/dist/core/recovery/checkpoint.js +33 -4
- package/dist/core/recovery/types.d.ts +1 -0
- package/dist/core/scrub.d.ts +17 -0
- package/dist/core/scrub.js +499 -5
- package/dist/core/standing-allow.d.ts +0 -20
- package/dist/core/standing-allow.js +0 -71
- package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
- package/dist/core/transactional/apply-observed-changes.js +11 -0
- package/dist/core/transactional/backend-selector.d.ts +2 -1
- package/dist/core/transactional/backend-selector.js +32 -29
- package/dist/core/transactional/backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +242 -54
- package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-git.js +53 -0
- package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
- package/dist/core/transactional/git-worktree-backend.js +3 -0
- package/dist/core/transactional/git-worktree.d.ts +1 -0
- package/dist/core/transactional/git-worktree.js +1 -0
- package/dist/core/transactional/runner.js +59 -13
- package/dist/core/types.d.ts +13 -0
- package/dist/core/verdict/egress-classify.js +1 -0
- package/dist/core/verdict/parser.d.ts +10 -0
- package/dist/core/verdict/parser.js +271 -40
- package/dist/core/verdict/shell-semantics.js +21 -0
- package/dist/corpus/adversarial-probe.d.ts +1 -1
- package/dist/corpus/adversarial-probe.js +3 -3
- package/dist/corpus/benign-probe-cores.d.ts +6 -0
- package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
- package/dist/corpus/coverage-compare.d.ts +36 -0
- package/dist/corpus/coverage-compare.js +197 -0
- package/dist/corpus/coverage-contexts.d.ts +17 -0
- package/dist/corpus/coverage-contexts.js +88 -0
- package/dist/corpus/coverage-matrix.d.ts +37 -0
- package/dist/corpus/coverage-matrix.js +159 -0
- package/dist/corpus/coverage-probe.d.ts +91 -0
- package/dist/corpus/coverage-probe.js +341 -0
- package/dist/corpus/evaluate.d.ts +1 -1
- package/dist/corpus/evaluate.js +1 -1
- package/dist/corpus/structural-fixture-root.d.ts +2 -0
- package/dist/corpus/structural-fixture-root.js +5 -0
- package/dist/defaults.js +0 -8
- package/dist/egress-daemon.js +20 -4
- package/dist/installer.js +3 -0
- package/dist/runtime-provenance.d.ts +10 -0
- package/dist/runtime-provenance.js +63 -0
- package/dist/templates.js +12 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
- package/skills/belay/SKILL.md +19 -0
- package/skills/belay/belay-status.md +5 -0
- package/dist/corpus/must-allow-commands.d.ts +0 -6
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { scrubString, scrubValue } from './scrub.js';
|
|
5
|
+
export const AUDIT_SCHEMA_VERSION = 3;
|
|
6
|
+
const ISO8601_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
|
|
7
|
+
const HEX64_PATTERN = /^[a-f0-9]{64}$/;
|
|
8
|
+
const SCRUB_PLACEHOLDERS = new Set(['<timestamp>', '<high-entropy>', '<approval-id>', '<uuid>']);
|
|
9
|
+
const PRESERVED_HASH_FIELDS = new Set([
|
|
10
|
+
'fingerprint',
|
|
11
|
+
'commandFingerprint',
|
|
12
|
+
'effectIRHash',
|
|
13
|
+
'payloadHash',
|
|
14
|
+
'configFingerprint',
|
|
15
|
+
'runtimeArtifactHash',
|
|
16
|
+
'decisionConfigFingerprint',
|
|
17
|
+
'receiptHash',
|
|
18
|
+
]);
|
|
19
|
+
const PRESERVED_LITERAL_FIELDS = new Set([
|
|
20
|
+
'timestamp',
|
|
21
|
+
'approvalCorrelationId',
|
|
22
|
+
'runtimeVersion',
|
|
23
|
+
'runtimeBuildStamp',
|
|
24
|
+
'boundaryProfile',
|
|
25
|
+
'schemaVersion',
|
|
26
|
+
'imageId',
|
|
27
|
+
'mirrorBackend',
|
|
28
|
+
'wouldMediate',
|
|
29
|
+
'exitCode',
|
|
30
|
+
'timedOut',
|
|
31
|
+
]);
|
|
32
|
+
const SCRUBBED_CONTAINER_FIELDS = new Set([
|
|
33
|
+
'summary',
|
|
34
|
+
'command',
|
|
35
|
+
'payload',
|
|
36
|
+
'replayContext',
|
|
37
|
+
'actionSnapshot',
|
|
38
|
+
'assessment',
|
|
39
|
+
'predictedAssessment',
|
|
40
|
+
'observedAssessment',
|
|
41
|
+
]);
|
|
42
|
+
export function approvalCorrelationId(approvalId) {
|
|
43
|
+
return createHash('sha256').update(approvalId).digest('hex').slice(0, 16);
|
|
44
|
+
}
|
|
45
|
+
export function isValidApprovalCorrelationId(value) {
|
|
46
|
+
return /^[a-f0-9]{16}$/.test(value);
|
|
47
|
+
}
|
|
48
|
+
export function isValidAuditTimestamp(value) {
|
|
49
|
+
if (SCRUB_PLACEHOLDERS.has(value) || !ISO8601_PATTERN.test(value)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return !Number.isNaN(Date.parse(value));
|
|
53
|
+
}
|
|
54
|
+
export function isValidAuditFingerprint(value) {
|
|
55
|
+
return HEX64_PATTERN.test(value) && !SCRUB_PLACEHOLDERS.has(value);
|
|
56
|
+
}
|
|
57
|
+
function isValidPreservedHashField(field, value) {
|
|
58
|
+
if (field === 'receiptHash') {
|
|
59
|
+
return HEX64_PATTERN.test(value);
|
|
60
|
+
}
|
|
61
|
+
if (field === 'imageId') {
|
|
62
|
+
return /^sha256:[a-f0-9]{64}$/.test(value);
|
|
63
|
+
}
|
|
64
|
+
return isValidAuditFingerprint(value);
|
|
65
|
+
}
|
|
66
|
+
function scrubAuditContainer(value, options) {
|
|
67
|
+
return scrubValue(value, {
|
|
68
|
+
...options,
|
|
69
|
+
maskHighEntropyStrings: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function serializeAuditField(key, value, options) {
|
|
73
|
+
if (value === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (key === 'approvalId') {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (key === 'ts' && typeof value === 'string' && isValidAuditTimestamp(value)) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (PRESERVED_LITERAL_FIELDS.has(key)) {
|
|
83
|
+
if (key === 'timestamp' && typeof value === 'string' && isValidAuditTimestamp(value)) {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
if (key === 'approvalCorrelationId' &&
|
|
87
|
+
typeof value === 'string' &&
|
|
88
|
+
isValidApprovalCorrelationId(value)) {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
if ((key === 'runtimeVersion' || key === 'runtimeBuildStamp' || key === 'boundaryProfile') &&
|
|
92
|
+
typeof value === 'string' &&
|
|
93
|
+
value.length > 0) {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
if (key === 'schemaVersion' && typeof value === 'number') {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
if (key === 'mirrorBackend' && value === 'file_copy') {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
if (key === 'wouldMediate' && value === true) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (key === 'exitCode' && (value === null || Number.isSafeInteger(value))) {
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
if (key === 'timedOut' && typeof value === 'boolean') {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
if (key === 'imageId' && typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value)) {
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (PRESERVED_HASH_FIELDS.has(key) && typeof value === 'string') {
|
|
117
|
+
return isValidPreservedHashField(key, value) ? value : undefined;
|
|
118
|
+
}
|
|
119
|
+
if (SCRUBBED_CONTAINER_FIELDS.has(key)) {
|
|
120
|
+
return scrubAuditContainer(value, options);
|
|
121
|
+
}
|
|
122
|
+
if (typeof value === 'string') {
|
|
123
|
+
return scrubString(value, { ...options, maskHighEntropyStrings: true });
|
|
124
|
+
}
|
|
125
|
+
if (value !== null && typeof value === 'object') {
|
|
126
|
+
return scrubAuditContainer(value, options);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
export function serializeAuditRecordV3(record, options) {
|
|
131
|
+
const timestamp = typeof record.timestamp === 'string' && isValidAuditTimestamp(record.timestamp)
|
|
132
|
+
? record.timestamp
|
|
133
|
+
: typeof record.ts === 'string' && isValidAuditTimestamp(record.ts)
|
|
134
|
+
? record.ts
|
|
135
|
+
: new Date().toISOString();
|
|
136
|
+
const serialized = {
|
|
137
|
+
schemaVersion: AUDIT_SCHEMA_VERSION,
|
|
138
|
+
timestamp,
|
|
139
|
+
};
|
|
140
|
+
if (typeof record.approvalId === 'string' && record.approvalId.length > 0) {
|
|
141
|
+
serialized.approvalCorrelationId = approvalCorrelationId(record.approvalId);
|
|
142
|
+
}
|
|
143
|
+
else if (typeof record.approvalCorrelationId === 'string' &&
|
|
144
|
+
isValidApprovalCorrelationId(record.approvalCorrelationId)) {
|
|
145
|
+
serialized.approvalCorrelationId = record.approvalCorrelationId;
|
|
146
|
+
}
|
|
147
|
+
for (const [key, value] of Object.entries(record)) {
|
|
148
|
+
if (key === 'timestamp' || key === 'ts' || key === 'approvalId' || key === 'schemaVersion') {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const next = serializeAuditField(key, value, options);
|
|
152
|
+
if (next !== undefined) {
|
|
153
|
+
serialized[key] = next;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return serialized;
|
|
157
|
+
}
|
|
158
|
+
export function parseAuditNdjsonLine(line) {
|
|
159
|
+
const trimmed = line.trim();
|
|
160
|
+
if (!trimmed) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(trimmed);
|
|
165
|
+
if (typeof parsed.ts === 'string' && parsed.timestamp === undefined) {
|
|
166
|
+
parsed.timestamp = parsed.ts;
|
|
167
|
+
delete parsed.ts;
|
|
168
|
+
}
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export async function appendAuditRecord(auditPath, record, options) {
|
|
176
|
+
await mkdir(path.dirname(auditPath), { recursive: true });
|
|
177
|
+
const line = JSON.stringify(serializeAuditRecordV3(record, options));
|
|
178
|
+
await appendFile(auditPath, `${line}\n`, 'utf8');
|
|
179
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AuditActionSnapshot, AuditReplayContext } from './audit-replay-context.js';
|
|
2
2
|
import type { Assessment } from './types.js';
|
|
3
3
|
export type { AuditActionSnapshot, AuditReplayContext } from './audit-replay-context.js';
|
|
4
|
-
export declare const AUDIT_METRICS_SCHEMA_VERSION =
|
|
4
|
+
export declare const AUDIT_METRICS_SCHEMA_VERSION = 4;
|
|
5
5
|
export declare const GATE_EVENTS: Set<string>;
|
|
6
6
|
export interface AuditRecord {
|
|
7
7
|
timestamp?: string;
|
|
@@ -42,6 +42,12 @@ export interface AuditRecord {
|
|
|
42
42
|
recoveryState?: string;
|
|
43
43
|
replayContext?: AuditReplayContext;
|
|
44
44
|
actionSnapshot?: AuditActionSnapshot;
|
|
45
|
+
wouldMediate?: boolean;
|
|
46
|
+
receiptHash?: string;
|
|
47
|
+
imageId?: string;
|
|
48
|
+
mirrorBackend?: 'file_copy';
|
|
49
|
+
exitCode?: number | null;
|
|
50
|
+
timedOut?: boolean;
|
|
45
51
|
[key: string]: unknown;
|
|
46
52
|
}
|
|
47
53
|
export interface AuditFilter {
|
|
@@ -63,6 +69,7 @@ export interface ApprovalRoundTrip {
|
|
|
63
69
|
approvalTimestamp?: string;
|
|
64
70
|
executeTimestamp?: string;
|
|
65
71
|
approvalId?: string;
|
|
72
|
+
approvalCorrelationId?: string;
|
|
66
73
|
fingerprint: string;
|
|
67
74
|
reason: string;
|
|
68
75
|
summary: string;
|
package/dist/core/audit-types.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const AUDIT_METRICS_SCHEMA_VERSION =
|
|
1
|
+
export const AUDIT_METRICS_SCHEMA_VERSION = 4;
|
|
2
2
|
export const GATE_EVENTS = new Set(['beforeShellExecution', 'preToolUse', 'subagentGate']);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const OUTPUT_TAIL_LIMIT_BYTES = 16384;
|
|
2
|
+
export interface BoundedOutputTail {
|
|
3
|
+
tail: Buffer;
|
|
4
|
+
truncated: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function appendBoundedOutputTail(current: Buffer, chunk: Buffer | string, limitBytes?: number): BoundedOutputTail;
|
|
7
|
+
export declare function decodeBoundedOutputTail(tail: Buffer): string;
|
|
8
|
+
export declare function boundedUtf8Tail(value: string, limitBytes?: number): {
|
|
9
|
+
value: string;
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const OUTPUT_TAIL_LIMIT_BYTES = 16_384;
|
|
2
|
+
export function appendBoundedOutputTail(current, chunk, limitBytes = OUTPUT_TAIL_LIMIT_BYTES) {
|
|
3
|
+
const next = Buffer.concat([current, Buffer.from(chunk)]);
|
|
4
|
+
return {
|
|
5
|
+
tail: next.length > limitBytes ? next.subarray(-limitBytes) : next,
|
|
6
|
+
truncated: next.length > limitBytes,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function decodeBoundedOutputTail(tail) {
|
|
10
|
+
for (let offset = 0; offset <= Math.min(3, tail.length); offset += 1) {
|
|
11
|
+
try {
|
|
12
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(tail.subarray(offset));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// A byte cap may have cut a leading multi-byte character; try its next boundary.
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return tail.toString('utf8');
|
|
19
|
+
}
|
|
20
|
+
export function boundedUtf8Tail(value, limitBytes = OUTPUT_TAIL_LIMIT_BYTES) {
|
|
21
|
+
const encoded = Buffer.from(value);
|
|
22
|
+
if (encoded.length <= limitBytes)
|
|
23
|
+
return { value, truncated: false };
|
|
24
|
+
const tail = encoded.subarray(-limitBytes);
|
|
25
|
+
return { value: decodeBoundedOutputTail(tail), truncated: true };
|
|
26
|
+
}
|
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
export declare const BOUNDARY_ATTESTATION_VERSION: 1;
|
|
2
|
+
export declare const CONTAINED_EXECUTION_ATTESTATION_VERSION: 1;
|
|
2
3
|
export type BoundaryDriverId = 'container' | 'cursor-sandbox' | 'seatbelt' | 'landlock' | 'host-integration';
|
|
4
|
+
export interface ContainedExecutionResourceLimits {
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
memoryMiB: number;
|
|
7
|
+
cpus: number;
|
|
8
|
+
pids: number;
|
|
9
|
+
}
|
|
10
|
+
export interface DockerSubstrateIdentity {
|
|
11
|
+
binaryPath: string;
|
|
12
|
+
binarySha256: string;
|
|
13
|
+
endpoint: string;
|
|
14
|
+
daemonId: string;
|
|
15
|
+
}
|
|
16
|
+
/** A separate execution-only capability; it does not attest L1 grant enforcement. */
|
|
17
|
+
export interface ContainedExecutionAttestation {
|
|
18
|
+
version: typeof CONTAINED_EXECUTION_ATTESTATION_VERSION;
|
|
19
|
+
imageId: string;
|
|
20
|
+
imageReference: string;
|
|
21
|
+
networkNone: true;
|
|
22
|
+
isolatesWorkspaceMirror: true;
|
|
23
|
+
readOnlyRoot: true;
|
|
24
|
+
sanitizedEnvironment: true;
|
|
25
|
+
dockerSubstrate: DockerSubstrateIdentity;
|
|
26
|
+
dockerConfiguration: {
|
|
27
|
+
executable: string;
|
|
28
|
+
host: string;
|
|
29
|
+
};
|
|
30
|
+
user: string;
|
|
31
|
+
entrypoint: '/bin/sh';
|
|
32
|
+
capDropAll: true;
|
|
33
|
+
noNewPrivileges: true;
|
|
34
|
+
logDriver: 'none';
|
|
35
|
+
proxyEnvironment: 'neutralized-empty';
|
|
36
|
+
tmpfs: {
|
|
37
|
+
path: '/tmp';
|
|
38
|
+
sizeBytes: number;
|
|
39
|
+
mode: 0o1777;
|
|
40
|
+
exec: false;
|
|
41
|
+
nosuid: true;
|
|
42
|
+
nodev: true;
|
|
43
|
+
};
|
|
44
|
+
memorySwapMiB: number;
|
|
45
|
+
shmSizeMiB: number;
|
|
46
|
+
healthcheckDisabled: true;
|
|
47
|
+
privateNamespaces: true;
|
|
48
|
+
privileged: false;
|
|
49
|
+
devicesNone: true;
|
|
50
|
+
resourceLimits: ContainedExecutionResourceLimits;
|
|
51
|
+
probedAt: string;
|
|
52
|
+
expiresAt: string;
|
|
53
|
+
}
|
|
3
54
|
export interface BoundaryAttestation {
|
|
4
55
|
version: typeof BOUNDARY_ATTESTATION_VERSION;
|
|
5
56
|
driver: BoundaryDriverId;
|
|
@@ -10,7 +61,12 @@ export interface BoundaryAttestation {
|
|
|
10
61
|
probeSignals: string[];
|
|
11
62
|
/** When true, the driver can mount an execution mirror at the original workspace path. */
|
|
12
63
|
isolatesWorkspaceMounts?: boolean;
|
|
64
|
+
/** Optional, execution-only proof. This must never be treated as an L1-full attestation. */
|
|
65
|
+
containedExecution?: ContainedExecutionAttestation;
|
|
13
66
|
}
|
|
14
67
|
export declare function validateBoundaryAttestation(value: unknown): value is BoundaryAttestation;
|
|
68
|
+
export declare function validateContainedExecutionAttestation(value: unknown): value is ContainedExecutionAttestation;
|
|
69
|
+
export declare function isContainedExecutionAttestationFresh(attestation: ContainedExecutionAttestation | null | undefined, now?: number): boolean;
|
|
70
|
+
export declare function isAttestationCurrent(attestation: BoundaryAttestation, now?: number): boolean;
|
|
15
71
|
export declare function isAttestationFresh(attestation: BoundaryAttestation, now?: number): boolean;
|
|
16
72
|
export declare function attestsWorkspaceMountIsolation(attestation: BoundaryAttestation | null | undefined): boolean;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
export const BOUNDARY_ATTESTATION_VERSION = 1;
|
|
3
|
+
export const CONTAINED_EXECUTION_ATTESTATION_VERSION = 1;
|
|
2
4
|
const KNOWN_DRIVERS = new Set([
|
|
3
5
|
'container',
|
|
4
6
|
'cursor-sandbox',
|
|
@@ -6,15 +8,21 @@ const KNOWN_DRIVERS = new Set([
|
|
|
6
8
|
'landlock',
|
|
7
9
|
'host-integration',
|
|
8
10
|
]);
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function isBoundaryDriver(value) {
|
|
15
|
+
return typeof value === 'string' && KNOWN_DRIVERS.has(value);
|
|
16
|
+
}
|
|
9
17
|
export function validateBoundaryAttestation(value) {
|
|
10
|
-
if (!value
|
|
18
|
+
if (!isRecord(value)) {
|
|
11
19
|
return false;
|
|
12
20
|
}
|
|
13
21
|
const record = value;
|
|
14
22
|
if (record.version !== BOUNDARY_ATTESTATION_VERSION) {
|
|
15
23
|
return false;
|
|
16
24
|
}
|
|
17
|
-
if (!
|
|
25
|
+
if (!isBoundaryDriver(record.driver)) {
|
|
18
26
|
return false;
|
|
19
27
|
}
|
|
20
28
|
if (typeof record.probedAt !== 'string' || typeof record.expiresAt !== 'string') {
|
|
@@ -34,6 +42,12 @@ export function validateBoundaryAttestation(value) {
|
|
|
34
42
|
typeof record.isolatesWorkspaceMounts !== 'boolean') {
|
|
35
43
|
return false;
|
|
36
44
|
}
|
|
45
|
+
if (record.containedExecution !== undefined) {
|
|
46
|
+
if (record.driver !== 'container' ||
|
|
47
|
+
!validateContainedExecutionAttestation(record.containedExecution)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
37
51
|
if (record.driver === 'host-integration' && record.materializesGrants) {
|
|
38
52
|
return false;
|
|
39
53
|
}
|
|
@@ -44,7 +58,94 @@ export function validateBoundaryAttestation(value) {
|
|
|
44
58
|
}
|
|
45
59
|
return true;
|
|
46
60
|
}
|
|
47
|
-
|
|
61
|
+
function hasPositiveLimit(value) {
|
|
62
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
63
|
+
}
|
|
64
|
+
function hasPositiveIntegerLimit(value) {
|
|
65
|
+
return hasPositiveLimit(value) && Number.isSafeInteger(value);
|
|
66
|
+
}
|
|
67
|
+
export function validateContainedExecutionAttestation(value) {
|
|
68
|
+
if (!isRecord(value)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const record = value;
|
|
72
|
+
const dockerSubstrate = record.dockerSubstrate;
|
|
73
|
+
const dockerConfiguration = record.dockerConfiguration;
|
|
74
|
+
if (record.version !== CONTAINED_EXECUTION_ATTESTATION_VERSION ||
|
|
75
|
+
typeof record.imageId !== 'string' ||
|
|
76
|
+
!/^sha256:[a-f0-9]{64}$/i.test(record.imageId) ||
|
|
77
|
+
typeof record.imageReference !== 'string' ||
|
|
78
|
+
!record.imageReference ||
|
|
79
|
+
/[\0\n\r]/.test(record.imageReference) ||
|
|
80
|
+
record.networkNone !== true ||
|
|
81
|
+
record.isolatesWorkspaceMirror !== true ||
|
|
82
|
+
record.readOnlyRoot !== true ||
|
|
83
|
+
record.sanitizedEnvironment !== true ||
|
|
84
|
+
!isRecord(dockerSubstrate) ||
|
|
85
|
+
typeof dockerSubstrate.binaryPath !== 'string' ||
|
|
86
|
+
!path.isAbsolute(dockerSubstrate.binaryPath) ||
|
|
87
|
+
/[\0\n\r]/.test(dockerSubstrate.binaryPath) ||
|
|
88
|
+
typeof dockerSubstrate.binarySha256 !== 'string' ||
|
|
89
|
+
!/^[a-f0-9]{64}$/.test(dockerSubstrate.binarySha256) ||
|
|
90
|
+
typeof dockerSubstrate.endpoint !== 'string' ||
|
|
91
|
+
!dockerSubstrate.endpoint.startsWith('unix:///') ||
|
|
92
|
+
!path.isAbsolute(dockerSubstrate.endpoint.slice('unix://'.length)) ||
|
|
93
|
+
/[\0\n\r]/.test(dockerSubstrate.endpoint) ||
|
|
94
|
+
typeof dockerSubstrate.daemonId !== 'string' ||
|
|
95
|
+
!dockerSubstrate.daemonId ||
|
|
96
|
+
/[\0\n\r]/.test(dockerSubstrate.daemonId) ||
|
|
97
|
+
!isRecord(dockerConfiguration) ||
|
|
98
|
+
typeof dockerConfiguration.executable !== 'string' ||
|
|
99
|
+
!path.isAbsolute(dockerConfiguration.executable) ||
|
|
100
|
+
/[\0\n\r]/.test(dockerConfiguration.executable) ||
|
|
101
|
+
typeof dockerConfiguration.host !== 'string' ||
|
|
102
|
+
!dockerConfiguration.host.startsWith('unix:///') ||
|
|
103
|
+
!path.isAbsolute(dockerConfiguration.host.slice('unix://'.length)) ||
|
|
104
|
+
/[\0\n\r]/.test(dockerConfiguration.host) ||
|
|
105
|
+
typeof record.user !== 'string' ||
|
|
106
|
+
!/^\d+:\d+$/.test(record.user) ||
|
|
107
|
+
record.entrypoint !== '/bin/sh' ||
|
|
108
|
+
record.capDropAll !== true ||
|
|
109
|
+
record.noNewPrivileges !== true ||
|
|
110
|
+
record.logDriver !== 'none' ||
|
|
111
|
+
record.proxyEnvironment !== 'neutralized-empty' ||
|
|
112
|
+
typeof record.probedAt !== 'string' ||
|
|
113
|
+
typeof record.expiresAt !== 'string') {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const probedAt = Date.parse(record.probedAt);
|
|
117
|
+
const expiresAt = Date.parse(record.expiresAt);
|
|
118
|
+
if (!Number.isFinite(probedAt) || !Number.isFinite(expiresAt) || expiresAt <= probedAt) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const limits = record.resourceLimits;
|
|
122
|
+
const tmpfs = record.tmpfs;
|
|
123
|
+
return (isRecord(tmpfs) &&
|
|
124
|
+
tmpfs.path === '/tmp' &&
|
|
125
|
+
hasPositiveIntegerLimit(tmpfs.sizeBytes) &&
|
|
126
|
+
tmpfs.mode === 0o1777 &&
|
|
127
|
+
tmpfs.exec === false &&
|
|
128
|
+
tmpfs.nosuid === true &&
|
|
129
|
+
tmpfs.nodev === true &&
|
|
130
|
+
hasPositiveIntegerLimit(record.memorySwapMiB) &&
|
|
131
|
+
hasPositiveIntegerLimit(record.shmSizeMiB) &&
|
|
132
|
+
record.healthcheckDisabled === true &&
|
|
133
|
+
record.privateNamespaces === true &&
|
|
134
|
+
record.privileged === false &&
|
|
135
|
+
record.devicesNone === true &&
|
|
136
|
+
isRecord(limits) &&
|
|
137
|
+
hasPositiveIntegerLimit(limits.timeoutMs) &&
|
|
138
|
+
hasPositiveIntegerLimit(limits.memoryMiB) &&
|
|
139
|
+
hasPositiveLimit(limits.cpus) &&
|
|
140
|
+
hasPositiveIntegerLimit(limits.pids));
|
|
141
|
+
}
|
|
142
|
+
export function isContainedExecutionAttestationFresh(attestation, now = Date.now()) {
|
|
143
|
+
// A contained proof must have been observed already; no clock skew is accepted.
|
|
144
|
+
return (validateContainedExecutionAttestation(attestation) &&
|
|
145
|
+
Date.parse(attestation.probedAt) <= now &&
|
|
146
|
+
Date.parse(attestation.expiresAt) > now);
|
|
147
|
+
}
|
|
148
|
+
export function isAttestationCurrent(attestation, now = Date.now()) {
|
|
48
149
|
const expires = Date.parse(attestation.expiresAt);
|
|
49
150
|
if (!Number.isFinite(expires)) {
|
|
50
151
|
return false;
|
|
@@ -52,6 +153,12 @@ export function isAttestationFresh(attestation, now = Date.now()) {
|
|
|
52
153
|
if (expires <= now) {
|
|
53
154
|
return false;
|
|
54
155
|
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
export function isAttestationFresh(attestation, now = Date.now()) {
|
|
159
|
+
if (!isAttestationCurrent(attestation, now)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
55
162
|
if (attestation.driver === 'host-integration') {
|
|
56
163
|
return true;
|
|
57
164
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BelayConfigV4 } from '../config.js';
|
|
2
|
+
import { type ContainedDockerDependencies } from '../contained-execution/docker.js';
|
|
2
3
|
import type { BoundaryAttestation, BoundaryDriverId } from './attestation.js';
|
|
3
4
|
import { type BoundaryDriver } from './boundary-driver.js';
|
|
4
5
|
import { dockerNetworkArgs } from './boundary-egress.js';
|
|
@@ -15,6 +16,7 @@ export interface ResolvedBoundaryDriverContext {
|
|
|
15
16
|
attestationPath: string;
|
|
16
17
|
attestation: BoundaryAttestation | null;
|
|
17
18
|
attestationFresh: boolean;
|
|
19
|
+
containedExecutionFresh?: boolean;
|
|
18
20
|
}
|
|
19
21
|
export declare function boundaryAttestationPath(repoRoot: string, config: BelayConfigV4): string;
|
|
20
22
|
export declare function loadBoundaryAttestation(filePath: string, expectedRepoRoot?: string, controlPlaneDir?: string): Promise<BoundaryAttestation | null>;
|
|
@@ -38,6 +40,7 @@ export declare function startBoundarySession(params: {
|
|
|
38
40
|
config: BelayConfigV4;
|
|
39
41
|
driverId?: BoundaryDriverId;
|
|
40
42
|
egressProxyRunning?: boolean;
|
|
43
|
+
containedDockerDependencies?: ContainedDockerDependencies;
|
|
41
44
|
}): Promise<{
|
|
42
45
|
attestation: BoundaryAttestation;
|
|
43
46
|
attestationPath: string;
|
|
@@ -45,10 +48,12 @@ export declare function startBoundarySession(params: {
|
|
|
45
48
|
export declare function boundarySessionStatus(params: {
|
|
46
49
|
repoRoot: string;
|
|
47
50
|
config: BelayConfigV4;
|
|
51
|
+
now?: number;
|
|
48
52
|
}): Promise<{
|
|
49
53
|
attestationPath: string;
|
|
50
54
|
attestation: BoundaryAttestation | null;
|
|
51
55
|
fresh: boolean;
|
|
56
|
+
containedExecutionFresh?: boolean;
|
|
52
57
|
}>;
|
|
53
58
|
export declare function runBoundaryAgentCommand(params: {
|
|
54
59
|
repoRoot: string;
|
|
@@ -2,11 +2,34 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { egressStatus } from '../../services/egress-service.js';
|
|
4
4
|
import { configuredControlPlaneDir } from '../config.js';
|
|
5
|
-
import {
|
|
5
|
+
import { probeContainedDockerForSession, } from '../contained-execution/docker.js';
|
|
6
|
+
import { isAttestationFresh, isContainedExecutionAttestationFresh } from './attestation.js';
|
|
6
7
|
import { readSignedAttestationFile, signBoundaryAttestation, verifySignedBoundaryAttestation, } from './boundary-attestation-sign.js';
|
|
7
8
|
import { getDefaultBoundaryDriver } from './boundary-driver.js';
|
|
8
9
|
import { dockerNetworkArgs, isEgressProxyActive, resolveBoundaryEgressProxyEnv, } from './boundary-egress.js';
|
|
9
10
|
import { runWithBoundaryRunnable, } from './boundary-run.js';
|
|
11
|
+
function containedExecutionFresh(attestation, config, now = Date.now()) {
|
|
12
|
+
if (attestation?.driver !== 'container' || !attestation.containedExecution)
|
|
13
|
+
return undefined;
|
|
14
|
+
const current = config.sandbox.containedExecution;
|
|
15
|
+
const capability = attestation.containedExecution;
|
|
16
|
+
const configCompatible = Boolean(current?.enabled &&
|
|
17
|
+
current.image === capability.imageReference &&
|
|
18
|
+
current.dockerExecutable === capability.dockerConfiguration.executable &&
|
|
19
|
+
current.dockerHost === capability.dockerConfiguration.host &&
|
|
20
|
+
current.timeoutMs === capability.resourceLimits.timeoutMs &&
|
|
21
|
+
current.memoryMiB === capability.resourceLimits.memoryMiB &&
|
|
22
|
+
current.cpus === capability.resourceLimits.cpus &&
|
|
23
|
+
current.pids === capability.resourceLimits.pids);
|
|
24
|
+
const probedAt = Date.parse(attestation.probedAt);
|
|
25
|
+
const expiresAt = Date.parse(attestation.expiresAt);
|
|
26
|
+
return (Number.isFinite(probedAt) &&
|
|
27
|
+
Number.isFinite(expiresAt) &&
|
|
28
|
+
probedAt <= now &&
|
|
29
|
+
expiresAt > now &&
|
|
30
|
+
configCompatible &&
|
|
31
|
+
isContainedExecutionAttestationFresh(capability, now));
|
|
32
|
+
}
|
|
10
33
|
export function boundaryAttestationPath(repoRoot, config) {
|
|
11
34
|
const rel = config.capability?.attestationRelPath ?? '.belay/attestation.json';
|
|
12
35
|
return path.isAbsolute(rel) ? rel : path.join(repoRoot, rel);
|
|
@@ -92,6 +115,7 @@ export async function resolveBoundaryDriverContext(params) {
|
|
|
92
115
|
attestationPath,
|
|
93
116
|
attestation,
|
|
94
117
|
attestationFresh: attestation ? isAttestationFresh(attestation) : false,
|
|
118
|
+
containedExecutionFresh: containedExecutionFresh(attestation, params.config),
|
|
95
119
|
};
|
|
96
120
|
}
|
|
97
121
|
export async function runWithBoundaryDriver(params) {
|
|
@@ -104,6 +128,18 @@ export async function runWithBoundaryDriver(params) {
|
|
|
104
128
|
});
|
|
105
129
|
}
|
|
106
130
|
export async function startBoundarySession(params) {
|
|
131
|
+
const contained = params.config.sandbox.containedExecution;
|
|
132
|
+
if (contained?.enabled === true) {
|
|
133
|
+
const attestation = await probeContainedDockerForSession({
|
|
134
|
+
repoRoot: params.repoRoot,
|
|
135
|
+
controlPlaneDir: configuredControlPlaneDir(params.config),
|
|
136
|
+
config: contained,
|
|
137
|
+
dependencies: params.containedDockerDependencies,
|
|
138
|
+
});
|
|
139
|
+
const attestationPath = boundaryAttestationPath(params.repoRoot, params.config);
|
|
140
|
+
await saveBoundaryAttestation(attestationPath, attestation, params.repoRoot, configuredControlPlaneDir(params.config));
|
|
141
|
+
return { attestation, attestationPath };
|
|
142
|
+
}
|
|
107
143
|
const resolved = await resolveBoundaryDriverContext(params);
|
|
108
144
|
const attestation = await resolved.driver.probe();
|
|
109
145
|
if (resolved.driver.prepare) {
|
|
@@ -120,6 +156,7 @@ export async function boundarySessionStatus(params) {
|
|
|
120
156
|
attestationPath,
|
|
121
157
|
attestation,
|
|
122
158
|
fresh: attestation ? isAttestationFresh(attestation) : false,
|
|
159
|
+
containedExecutionFresh: containedExecutionFresh(attestation, params.config, params.now),
|
|
123
160
|
};
|
|
124
161
|
}
|
|
125
162
|
export async function runBoundaryAgentCommand(params) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
|
|
2
|
-
export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, type BoundaryAttestation, type BoundaryDriverId, isAttestationFresh, } from './attestation.js';
|
|
2
|
+
export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, type BoundaryAttestation, type BoundaryDriverId, CONTAINED_EXECUTION_ATTESTATION_VERSION, type ContainedExecutionAttestation, type ContainedExecutionResourceLimits, isAttestationFresh, isContainedExecutionAttestationFresh, validateContainedExecutionAttestation, } from './attestation.js';
|
|
3
3
|
export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
|
|
4
4
|
export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
|
|
5
5
|
export { CAPABILITY_GRANT_VERSION, type CapabilityGrantV1, isGrantScopeTooBroad, } from './grant.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
|
|
2
|
-
export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, isAttestationFresh, } from './attestation.js';
|
|
2
|
+
export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, CONTAINED_EXECUTION_ATTESTATION_VERSION, isAttestationFresh, isContainedExecutionAttestationFresh, validateContainedExecutionAttestation, } from './attestation.js';
|
|
3
3
|
export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
|
|
4
4
|
export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
|
|
5
5
|
export { CAPABILITY_GRANT_VERSION, isGrantScopeTooBroad, } from './grant.js';
|
|
@@ -40,17 +40,11 @@ function effectPathIsLocal(target, context) {
|
|
|
40
40
|
const resolved = resolveCapabilityPath(target, context.cwd);
|
|
41
41
|
return (resolveWorkspaceRootMatch(context.repoRoot, [...(context.trustedWorkspaceRoots ?? [])], resolved) !== null);
|
|
42
42
|
}
|
|
43
|
-
function
|
|
43
|
+
function effectPathIsSecretOrCredentialPath(target, context) {
|
|
44
44
|
const resolved = resolveCapabilityPath(target, context.cwd);
|
|
45
45
|
if (HIGH_STAKES_EFFECT_PATHS.some((pattern) => pattern.test(resolved))) {
|
|
46
46
|
return true;
|
|
47
47
|
}
|
|
48
|
-
if (context.protectedArtifactRoots?.some((root) => pathWithinRoot(canonicalPath(root), resolved))) {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
if (isGitMetadataPath(resolved, context.repoRoot)) {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
48
|
const workspaceMatch = resolveWorkspaceRootMatch(context.repoRoot, [...(context.trustedWorkspaceRoots ?? [])], resolved);
|
|
55
49
|
if (!workspaceMatch) {
|
|
56
50
|
return false;
|
|
@@ -59,6 +53,24 @@ function effectPathIsHighStakes(target, context) {
|
|
|
59
53
|
...(context.sensitivePaths ?? []),
|
|
60
54
|
]);
|
|
61
55
|
}
|
|
56
|
+
function effectPathIsControlPlaneArtifactPath(target, context) {
|
|
57
|
+
const resolved = resolveCapabilityPath(target, context.cwd);
|
|
58
|
+
return (context.protectedArtifactRoots?.some((root) => pathWithinRoot(canonicalPath(root), resolved)) ??
|
|
59
|
+
false);
|
|
60
|
+
}
|
|
61
|
+
function effectPathIsHighStakes(target, context) {
|
|
62
|
+
if (effectPathIsSecretOrCredentialPath(target, context)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (effectPathIsControlPlaneArtifactPath(target, context)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
const resolved = resolveCapabilityPath(target, context.cwd);
|
|
69
|
+
if (isGitMetadataPath(resolved, context.repoRoot)) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
62
74
|
/**
|
|
63
75
|
* Policy for a lowered shell effect. It intentionally ignores executable
|
|
64
76
|
* command names, segment heads, command text, and config allowlists.
|
|
@@ -115,12 +127,13 @@ export function evaluateEffectRequirementPolicy(requirement, context) {
|
|
|
115
127
|
}
|
|
116
128
|
switch (requirement.action) {
|
|
117
129
|
case 'fs.read':
|
|
118
|
-
if (requirement.resource.kind === 'path'
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
if (requirement.resource.kind === 'path') {
|
|
131
|
+
if (effectPathIsSecretOrCredentialPath(requirement.resource.path, context)) {
|
|
132
|
+
return effectDecision('ask', 'high_stakes_path', 'effect.fs_read_high_stakes', [
|
|
133
|
+
...signals,
|
|
134
|
+
'sensitive_path_read',
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
124
137
|
}
|
|
125
138
|
return effectDecision('allow', 'read_only', 'effect.fs_read', signals);
|
|
126
139
|
case 'fs.write':
|
|
@@ -130,6 +143,12 @@ export function evaluateEffectRequirementPolicy(requirement, context) {
|
|
|
130
143
|
if (requirement.resource.kind !== 'path') {
|
|
131
144
|
return effectDecision('ask', 'indeterminate_effect', 'effect.fs_write_unknown', signals);
|
|
132
145
|
}
|
|
146
|
+
if (effectPathIsControlPlaneArtifactPath(requirement.resource.path, context)) {
|
|
147
|
+
return effectDecision('ask', 'control_plane_mutation', 'effect.control_plane_write', [
|
|
148
|
+
...signals,
|
|
149
|
+
'control_plane_path',
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
133
152
|
if (effectPathIsHighStakes(requirement.resource.path, context)) {
|
|
134
153
|
return effectDecision('ask', 'high_stakes_path', 'effect.fs_write_high_stakes', [
|
|
135
154
|
...signals,
|