polydeukes 0.6.0 → 0.7.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.ko.md +30 -70
- package/README.md +31 -84
- package/dist/baseline.d.ts +37 -1
- package/dist/baseline.js +68 -1
- package/dist/bin.d.ts +3 -4
- package/dist/bin.js +79 -93
- package/dist/covenant/bash-line.d.ts +130 -0
- package/dist/covenant/bash-line.js +566 -0
- package/dist/covenant/declaration-engine.d.ts +108 -0
- package/dist/covenant/declaration-engine.js +415 -0
- package/dist/covenant/discipline.d.ts +103 -0
- package/dist/covenant/discipline.js +667 -0
- package/dist/covenant/dispatch.d.ts +153 -0
- package/dist/covenant/dispatch.js +237 -0
- package/dist/covenant/extract-steps.d.ts +122 -0
- package/dist/covenant/extract-steps.js +0 -0
- package/dist/covenant/mention.d.ts +117 -0
- package/dist/covenant/mention.js +209 -0
- package/dist/covenant/module.d.ts +25 -0
- package/dist/covenant/module.js +23 -0
- package/dist/covenant/mutation-rules.d.ts +40 -0
- package/dist/covenant/mutation-rules.js +146 -0
- package/dist/covenant/relations.d.ts +46 -0
- package/dist/covenant/relations.js +68 -0
- package/dist/covenant/run-covenant.d.ts +95 -0
- package/dist/covenant/run-covenant.js +164 -0
- package/dist/covenant/self-mod.d.ts +55 -0
- package/dist/covenant/self-mod.js +84 -0
- package/dist/covenant/shell-evidence.d.ts +38 -0
- package/dist/covenant/shell-evidence.js +247 -0
- package/dist/covenant/shell-mod.d.ts +84 -0
- package/dist/covenant/shell-mod.js +213 -0
- package/dist/covenant/supply.d.ts +54 -0
- package/dist/covenant/supply.js +50 -0
- package/dist/covenant/transcript-mod.d.ts +60 -0
- package/dist/covenant/transcript-mod.js +254 -0
- package/dist/covenant/ttl-witness.d.ts +47 -0
- package/dist/covenant/ttl-witness.js +80 -0
- package/dist/covenant-check.d.ts +72 -39
- package/dist/covenant-check.js +260 -121
- package/dist/diff-ir.d.ts +28 -0
- package/dist/diff-ir.js +310 -0
- package/dist/docs/README.ko.md +60 -0
- package/dist/docs/README.md +64 -0
- package/dist/docs/catalog.json +480 -0
- package/dist/docs/concepts/judgment.ko.md +113 -0
- package/dist/docs/concepts/judgment.md +113 -0
- package/dist/docs/how-to/configure-project.ko.md +99 -0
- package/dist/docs/how-to/configure-project.md +98 -0
- package/dist/docs/how-to/connect-surfaces.ko.md +120 -0
- package/dist/docs/how-to/connect-surfaces.md +121 -0
- package/dist/docs/how-to/write-disciplines.ko.md +179 -0
- package/dist/docs/how-to/write-disciplines.md +181 -0
- package/dist/docs/index.json +2280 -0
- package/dist/docs/reference/cli/covenant-check.ko.md +148 -0
- package/dist/docs/reference/cli/covenant-check.md +153 -0
- package/dist/docs/reference/cli/docs.ko.md +97 -0
- package/dist/docs/reference/cli/docs.md +95 -0
- package/dist/docs/reference/cli/explain.ko.md +84 -0
- package/dist/docs/reference/cli/explain.md +89 -0
- package/dist/docs/reference/cli/init.ko.md +119 -0
- package/dist/docs/reference/cli/init.md +119 -0
- package/dist/docs/reference/configuration/index.ko.md +553 -0
- package/dist/docs/reference/{configuration.md → configuration/index.md} +215 -90
- package/dist/docs/reference/packages/adapter-claude-code.ko.md +110 -0
- package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +47 -19
- package/dist/docs/reference/packages/adapter-grok.ko.md +77 -0
- package/dist/docs/reference/packages/adapter-grok.md +79 -0
- package/dist/docs/reference/packages/core.ko.md +174 -0
- package/dist/docs/reference/{core.md → packages/core.md} +74 -12
- package/dist/docs/reference/packages/polydeukes.ko.md +195 -0
- package/dist/docs/reference/packages/polydeukes.md +204 -0
- package/dist/docs/reference/packages/sdk-ts.ko.md +161 -0
- package/dist/docs/reference/packages/sdk-ts.md +164 -0
- package/dist/docs/troubleshooting.ko.md +144 -0
- package/dist/docs/troubleshooting.md +103 -151
- package/dist/docs/tutorials/first-judgment.ko.md +82 -0
- package/dist/docs/tutorials/first-judgment.md +81 -0
- package/dist/docs-catalog.d.ts +25 -0
- package/dist/docs-catalog.js +450 -0
- package/dist/docs-library.d.ts +23 -0
- package/dist/docs-library.js +347 -0
- package/dist/docs-markdown.d.ts +32 -0
- package/dist/docs-markdown.js +150 -0
- package/dist/docs-query.d.ts +11 -40
- package/dist/docs-query.js +28 -122
- package/dist/docs-types.d.ts +105 -0
- package/dist/docs-types.js +2 -0
- package/dist/explain.d.ts +1 -1
- package/dist/explain.js +37 -22
- package/dist/load-config.d.ts +1 -1
- package/dist/load-config.js +1 -1
- package/dist/pre-state-reader.d.ts +1 -1
- package/dist/pre-state-reader.js +1 -1
- package/dist/scaffold-project.d.ts +2 -2
- package/dist/scaffold-project.js +9 -3
- package/dist/schema/polydeukes.schema.json +12 -0
- package/dist/worktree-reader.d.ts +19 -0
- package/dist/worktree-reader.js +30 -0
- package/package.json +3 -17
- package/dist/claude-code-hook.d.ts +0 -79
- package/dist/claude-code-hook.js +0 -372
- package/dist/claude-code.d.ts +0 -6
- package/dist/claude-code.js +0 -6
- package/dist/covenant-module.d.ts +0 -25
- package/dist/covenant-module.js +0 -42
- package/dist/docs/configuration.md +0 -103
- package/dist/docs/installation.md +0 -241
- package/dist/docs/reference/adapter-git.md +0 -100
- package/dist/docs/reference/covenant.md +0 -116
- package/dist/docs/reference/polydeukes.md +0 -315
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -21
- package/dist/init-claude-code.d.ts +0 -52
- package/dist/init-claude-code.js +0 -469
- package/dist/init-grok.d.ts +0 -51
- package/dist/init-grok.js +0 -242
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runCovenant` — the covenant execution wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Calls an in-process judge thunk the assembly has already bound its payload and options
|
|
5
|
+
* into, translates the thunk's exit-code equivalent by policy (1 → blocking 2), writes the
|
|
6
|
+
* break reason to stderr, and appends exactly one telemetry record per call via
|
|
7
|
+
* {@link appendRecordFailOpen} (the core's fail-open wrapper around its sole collector —
|
|
8
|
+
* no local logger). {@link translateExitCode} is pure.
|
|
9
|
+
*/
|
|
10
|
+
import { appendRecordFailOpen, EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD, } from '@polydeukes/core';
|
|
11
|
+
/**
|
|
12
|
+
* How many witness elements one break contributes to a telemetry row. A relation over a
|
|
13
|
+
* large file can return thousands, and the row is one line — the true count rides beside
|
|
14
|
+
* the truncated list rather than being lost with it.
|
|
15
|
+
*/
|
|
16
|
+
const WITNESSES_PER_BREAK = 8;
|
|
17
|
+
/**
|
|
18
|
+
* How many characters of one witness value the row keeps. A witness over a bare `source`
|
|
19
|
+
* step is the whole file, and the row is one line — the value is cut with its true length
|
|
20
|
+
* beside it rather than dropped.
|
|
21
|
+
*/
|
|
22
|
+
const WITNESS_VALUE_CHARS = 200;
|
|
23
|
+
/** A witness whose serialized value fits the row; a longer one is cut and says so. */
|
|
24
|
+
function boundedWitness(witness) {
|
|
25
|
+
const serialized = JSON.stringify(witness.value);
|
|
26
|
+
if (serialized === undefined || serialized.length <= WITNESS_VALUE_CHARS)
|
|
27
|
+
return witness;
|
|
28
|
+
return {
|
|
29
|
+
...witness,
|
|
30
|
+
value: serialized.slice(0, WITNESS_VALUE_CHARS),
|
|
31
|
+
truncated: serialized.length,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Serialize a body's breaks into the row's fifth field: id, capped witnesses, true total —
|
|
36
|
+
* or nothing when a value cannot be serialized. Telemetry is fail-open, so a witness
|
|
37
|
+
* carrying a value `JSON.stringify` refuses costs the row its fifth field, never the verdict.
|
|
38
|
+
*/
|
|
39
|
+
function serializeWitnesses(breaks) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.stringify(breaks.map((entry) => ({
|
|
42
|
+
id: entry.id,
|
|
43
|
+
witnesses: entry.witnesses.slice(0, WITNESSES_PER_BREAK).map(boundedWitness),
|
|
44
|
+
total: entry.witnesses.length,
|
|
45
|
+
})));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Translate a body outcome into the wrapper verdict and telemetry event (pure).
|
|
53
|
+
*
|
|
54
|
+
* `bodyExitCode === 0` (uphold) passes; every other outcome — a break report (`1`), the
|
|
55
|
+
* body's own fail-closed (`2`), any uninterpretable code (`3+`), or a body that answered
|
|
56
|
+
* nothing interpretable (`null`) — is fail-closed to the blocking `2` / `blocked`. The
|
|
57
|
+
* unconditional 1→2 translation lives here, isolated, so it has one place to evolve in.
|
|
58
|
+
*
|
|
59
|
+
* `enforce` relaxes ONLY the verdict cell: under `advise` a break report (`1`) becomes
|
|
60
|
+
* `0` / `advised` — recorded, not blocking. Every unjudgeable outcome (`2`, `3+`, `null`)
|
|
61
|
+
* stays `2` / `blocked` regardless of level.
|
|
62
|
+
*/
|
|
63
|
+
export function translateExitCode(bodyExitCode, enforce = 'block') {
|
|
64
|
+
if (bodyExitCode === EXIT_UPHOLD) {
|
|
65
|
+
return { exitCode: EXIT_UPHOLD, event: 'passed' };
|
|
66
|
+
}
|
|
67
|
+
if (enforce === 'advise' && bodyExitCode === EXIT_BREAK_NON_BLOCKING) {
|
|
68
|
+
return { exitCode: EXIT_UPHOLD, event: 'advised' };
|
|
69
|
+
}
|
|
70
|
+
return { exitCode: EXIT_BREAK_BLOCKING, event: 'blocked' };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Run the judge thunk and normalize what it answers.
|
|
74
|
+
*
|
|
75
|
+
* A throw is the body-crash cell, so crash isolation is this try/catch. A resolution that
|
|
76
|
+
* is not the outcome shape (a stale dist answering an older contract) is uninterpretable
|
|
77
|
+
* and lands in the same cell: `null` routes to the translation table's fail-closed row
|
|
78
|
+
* without that table learning a new code.
|
|
79
|
+
*/
|
|
80
|
+
async function runBody(body) {
|
|
81
|
+
let outcome;
|
|
82
|
+
try {
|
|
83
|
+
outcome = await body();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return { exitCode: EXIT_BREAK_BLOCKING };
|
|
87
|
+
}
|
|
88
|
+
if (typeof outcome !== 'object' || outcome === null) {
|
|
89
|
+
return { exitCode: EXIT_BREAK_BLOCKING };
|
|
90
|
+
}
|
|
91
|
+
return outcome;
|
|
92
|
+
}
|
|
93
|
+
/** Turn a pure judge's verdict into the outcome a thunk answers. */
|
|
94
|
+
export function outcomeFromVerdict(verdict) {
|
|
95
|
+
return verdict.upheld
|
|
96
|
+
? { exitCode: EXIT_UPHOLD }
|
|
97
|
+
: { exitCode: EXIT_BREAK_NON_BLOCKING, reason: verdict.reason };
|
|
98
|
+
}
|
|
99
|
+
/** The unjudgeable outcome: a misassembly or an input no judge could read (`2`, no reason). */
|
|
100
|
+
export const UNJUDGEABLE_OUTCOME = { exitCode: EXIT_BREAK_BLOCKING };
|
|
101
|
+
/** Consult the valve, counting a throw as closed — an uncertain valve never opens. */
|
|
102
|
+
function witnessOpens(witness) {
|
|
103
|
+
try {
|
|
104
|
+
return witness() === true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Run a covenant body through the wrapper.
|
|
112
|
+
*
|
|
113
|
+
* The order is judge → translate → valve: the body always runs, and only a `blocked`
|
|
114
|
+
* translation has anything for the valve to relax into `0` / `witnessed`. Whatever that
|
|
115
|
+
* leaves is recorded ONCE — one call, one row — so a witnessed break never leaves a
|
|
116
|
+
* `blocked` row beside its `witnessed` one.
|
|
117
|
+
*
|
|
118
|
+
* The break reason goes to stderr whenever the thunk carried one, whatever the level and
|
|
119
|
+
* whatever the final event: gating it on the verdict would leave `advised` mute and the
|
|
120
|
+
* valve silent about what it opened.
|
|
121
|
+
*
|
|
122
|
+
* A body that answers `skipped` evaluated no relation, so it never reaches the translation
|
|
123
|
+
* table or the valve: the row says so and the call upholds.
|
|
124
|
+
*
|
|
125
|
+
* Resolves with the wrapper's final `exitCode` (`0` or `2`) and the telemetry `event` that
|
|
126
|
+
* was recorded. The event is surfaced rather than left to callers: the valve is impure, so
|
|
127
|
+
* recomputing the event would consult it a second time. Logging is fail-open
|
|
128
|
+
* via {@link appendRecordFailOpen}: a telemetry failure never alters the verdict and never
|
|
129
|
+
* throws. The gate closes; the measurement stays open.
|
|
130
|
+
*/
|
|
131
|
+
export async function runCovenant(spec) {
|
|
132
|
+
const outcome = await runBody(spec.body);
|
|
133
|
+
// A code the outcome does not carry as a number is uninterpretable, and `null` is the
|
|
134
|
+
// table's existing cell for exactly that — no new row in translateExitCode.
|
|
135
|
+
const bodyExitCode = typeof outcome.exitCode === 'number' ? outcome.exitCode : null;
|
|
136
|
+
if (typeof outcome.reason === 'string' && outcome.reason !== '') {
|
|
137
|
+
process.stderr.write(`${outcome.reason}\n`);
|
|
138
|
+
}
|
|
139
|
+
// A skip token beside a non-zero code is a body contradicting itself; the break wins, so a
|
|
140
|
+
// verdict is never downgraded by a stray token.
|
|
141
|
+
if (outcome.skipped !== undefined && bodyExitCode === EXIT_UPHOLD) {
|
|
142
|
+
appendRecordFailOpen(spec.telemetryPath, {
|
|
143
|
+
event: 'skipped',
|
|
144
|
+
label: spec.label,
|
|
145
|
+
subject: spec.subject ?? '-',
|
|
146
|
+
reason: outcome.skipped,
|
|
147
|
+
});
|
|
148
|
+
return { exitCode: EXIT_UPHOLD, event: 'skipped' };
|
|
149
|
+
}
|
|
150
|
+
const verdict = translateExitCode(bodyExitCode, spec.enforce);
|
|
151
|
+
const { exitCode, event } = verdict.event === 'blocked' && spec.witness !== undefined && witnessOpens(spec.witness)
|
|
152
|
+
? { exitCode: EXIT_UPHOLD, event: 'witnessed' }
|
|
153
|
+
: verdict;
|
|
154
|
+
const serialized = Array.isArray(outcome.witnesses)
|
|
155
|
+
? serializeWitnesses(outcome.witnesses)
|
|
156
|
+
: undefined;
|
|
157
|
+
appendRecordFailOpen(spec.telemetryPath, {
|
|
158
|
+
event,
|
|
159
|
+
label: spec.label,
|
|
160
|
+
subject: spec.subject ?? '-',
|
|
161
|
+
...(serialized !== undefined && { witnesses: serialized }),
|
|
162
|
+
});
|
|
163
|
+
return { exitCode, event };
|
|
164
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `judgeSelfModification` — the self-mod meta-covenant's pure judge (zero I/O).
|
|
3
|
+
*
|
|
4
|
+
* Breaks when a *mutating* tool call (a `name` exactly equal to an injected entry of
|
|
5
|
+
* `mutatingToolNames`) targets a protected path. The target is read from the call's own
|
|
6
|
+
* nested `fileChange` evidence when it carries one, and only otherwise from an `args`
|
|
7
|
+
* mention traversal. It judges only its own axis: a non-mutating tool call that merely
|
|
8
|
+
* mentions a protected path is upheld — that path belongs to the Bash meta-covenant, and
|
|
9
|
+
* run-all co-existence depends on this boundary. Tool names and paths are injected values,
|
|
10
|
+
* never source literals.
|
|
11
|
+
*/
|
|
12
|
+
import type { CovenantInput, CovenantVerdict } from '@polydeukes/core';
|
|
13
|
+
import type { CovenantRegistration, MetaCovenantRegistration } from './dispatch.ts';
|
|
14
|
+
/**
|
|
15
|
+
* `SelfModificationSpec` — the injected axes of the judge.
|
|
16
|
+
*
|
|
17
|
+
* `protectedPaths` are literal path strings; `mutatingToolNames` are the tool names that
|
|
18
|
+
* count as mutating. Empty-string entries in either list are ignored (an unguarded `''`
|
|
19
|
+
* would match every path / every tool).
|
|
20
|
+
*/
|
|
21
|
+
export type SelfModificationSpec = {
|
|
22
|
+
protectedPaths: string[];
|
|
23
|
+
mutatingToolNames: string[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Judge a {@link CovenantInput} against the self-mod spec (pure).
|
|
27
|
+
*
|
|
28
|
+
* A call whose `name` is a non-empty `mutatingToolNames` entry is judged on its proven
|
|
29
|
+
* `fileChange` target when it carries one ({@link pathMatchesProtected} segment semantics,
|
|
30
|
+
* every kind including `delete`), and otherwise on an `args` mention traversal at any
|
|
31
|
+
* depth. Evidence, when attached, must be the call's complete mutation-target set.
|
|
32
|
+
*
|
|
33
|
+
* @param input - The call set to judge
|
|
34
|
+
* @param spec - Protected paths and mutating tool names; empty strings are ignored
|
|
35
|
+
* @returns A break naming the tool and the proven or mentioned path, or an uphold
|
|
36
|
+
*/
|
|
37
|
+
export declare function judgeSelfModification(input: CovenantInput, spec: SelfModificationSpec): CovenantVerdict;
|
|
38
|
+
/**
|
|
39
|
+
* `SelfModRegistrationSpec` — the assembly values baked into the registration. The call
|
|
40
|
+
* set is not among them: the dispatcher supplies it to the judge at call time, so one
|
|
41
|
+
* built registration serves every payload.
|
|
42
|
+
*/
|
|
43
|
+
export type SelfModRegistrationSpec = {
|
|
44
|
+
protectedPaths: string[];
|
|
45
|
+
mutatingToolNames: string[];
|
|
46
|
+
witness?: CovenantRegistration['witness'];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Build the self-mod registration. Routing stays path mention; the judgment is the thunk.
|
|
50
|
+
*
|
|
51
|
+
* The misassembly gate lives at the thunk's entry: zero valid entries in either list would
|
|
52
|
+
* make {@link judgeSelfModification} uphold every call, so it answers the unjudgeable
|
|
53
|
+
* outcome instead, which no enforce level softens.
|
|
54
|
+
*/
|
|
55
|
+
export declare function selfModRegistration(spec: SelfModRegistrationSpec): MetaCovenantRegistration;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `judgeSelfModification` — the self-mod meta-covenant's pure judge (zero I/O).
|
|
3
|
+
*
|
|
4
|
+
* Breaks when a *mutating* tool call (a `name` exactly equal to an injected entry of
|
|
5
|
+
* `mutatingToolNames`) targets a protected path. The target is read from the call's own
|
|
6
|
+
* nested `fileChange` evidence when it carries one, and only otherwise from an `args`
|
|
7
|
+
* mention traversal. It judges only its own axis: a non-mutating tool call that merely
|
|
8
|
+
* mentions a protected path is upheld — that path belongs to the Bash meta-covenant, and
|
|
9
|
+
* run-all co-existence depends on this boundary. Tool names and paths are injected values,
|
|
10
|
+
* never source literals.
|
|
11
|
+
*/
|
|
12
|
+
import { mentionsPath, pathMatchesProtected, provenChangePath } from './mention.js';
|
|
13
|
+
import { outcomeFromVerdict, UNJUDGEABLE_OUTCOME } from './run-covenant.js';
|
|
14
|
+
/**
|
|
15
|
+
* Judge a {@link CovenantInput} against the self-mod spec (pure).
|
|
16
|
+
*
|
|
17
|
+
* A call whose `name` is a non-empty `mutatingToolNames` entry is judged on its proven
|
|
18
|
+
* `fileChange` target when it carries one ({@link pathMatchesProtected} segment semantics,
|
|
19
|
+
* every kind including `delete`), and otherwise on an `args` mention traversal at any
|
|
20
|
+
* depth. Evidence, when attached, must be the call's complete mutation-target set.
|
|
21
|
+
*
|
|
22
|
+
* @param input - The call set to judge
|
|
23
|
+
* @param spec - Protected paths and mutating tool names; empty strings are ignored
|
|
24
|
+
* @returns A break naming the tool and the proven or mentioned path, or an uphold
|
|
25
|
+
*/
|
|
26
|
+
export function judgeSelfModification(input, spec) {
|
|
27
|
+
const mutatingNames = spec.mutatingToolNames.filter((name) => name !== '');
|
|
28
|
+
const protectedPaths = spec.protectedPaths.filter((path) => path !== '');
|
|
29
|
+
for (const call of input.toolCalls) {
|
|
30
|
+
if (!mutatingNames.includes(call.name)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const changePath = provenChangePath(call);
|
|
34
|
+
if (changePath !== null) {
|
|
35
|
+
if (protectedPaths.some((path) => pathMatchesProtected(changePath, path))) {
|
|
36
|
+
return {
|
|
37
|
+
upheld: false,
|
|
38
|
+
reason: `${call.name} would modify protected path ${changePath}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const mentioned = protectedPaths.find((path) => mentionsPath(call.args, path));
|
|
44
|
+
if (mentioned !== undefined) {
|
|
45
|
+
return {
|
|
46
|
+
upheld: false,
|
|
47
|
+
reason: `${call.name} would modify protected path ${mentioned}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { upheld: true };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the self-mod registration. Routing stays path mention; the judgment is the thunk.
|
|
55
|
+
*
|
|
56
|
+
* The misassembly gate lives at the thunk's entry: zero valid entries in either list would
|
|
57
|
+
* make {@link judgeSelfModification} uphold every call, so it answers the unjudgeable
|
|
58
|
+
* outcome instead, which no enforce level softens.
|
|
59
|
+
*/
|
|
60
|
+
export function selfModRegistration(spec) {
|
|
61
|
+
const judgeSpec = {
|
|
62
|
+
protectedPaths: spec.protectedPaths,
|
|
63
|
+
mutatingToolNames: spec.mutatingToolNames,
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
label: 'self-mod',
|
|
67
|
+
protectedPaths: spec.protectedPaths,
|
|
68
|
+
body: async (input) => {
|
|
69
|
+
if (judgeSpec.protectedPaths.filter((path) => path !== '').length === 0 ||
|
|
70
|
+
judgeSpec.mutatingToolNames.filter((name) => name !== '').length === 0) {
|
|
71
|
+
return UNJUDGEABLE_OUTCOME;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return outcomeFromVerdict(judgeSelfModification(input, judgeSpec));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Structurally unjudgeable input that passed parseInput (which validates the
|
|
78
|
+
// collection shapes, not the element ones): cannot judge means block.
|
|
79
|
+
return UNJUDGEABLE_OUTCOME;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
...(spec.witness !== undefined ? { witness: spec.witness } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell-evidence derivation — one command line in, computed writes plus a reasoned
|
|
3
|
+
* unjudgeable list out.
|
|
4
|
+
*
|
|
5
|
+
* Pure: no disk read, no spawn. `create`/`modify` discrimination and the append composition
|
|
6
|
+
* need a pre-state, which belongs to the judged body; this layer answers only what the
|
|
7
|
+
* command text itself decides.
|
|
8
|
+
*
|
|
9
|
+
* The enumerated forms below ARE the contract — a form none of them names is unjudgeable,
|
|
10
|
+
* never a confident guess. An uncomputable write whose target is known carries that path,
|
|
11
|
+
* so the compiler can route it to the discipline whose scope covers it; a write whose
|
|
12
|
+
* target is unknowable carries none. A command with no mutation signal at all leaves both
|
|
13
|
+
* lists empty: recording a line per read would drown the log it feeds.
|
|
14
|
+
*/
|
|
15
|
+
/** A computed write: the target, the exact bytes written, and the redirect direction. */
|
|
16
|
+
export type ShellChange = {
|
|
17
|
+
path: string;
|
|
18
|
+
content: string;
|
|
19
|
+
mode: 'truncate' | 'append';
|
|
20
|
+
};
|
|
21
|
+
/** A detected write that cannot be computed; `path` is present only when the target is known. */
|
|
22
|
+
export type ShellUnjudgeable = {
|
|
23
|
+
path?: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
};
|
|
26
|
+
/** The derivation of one command line: computed writes and detected-but-uncomputable ones. */
|
|
27
|
+
export type ShellDerivation = {
|
|
28
|
+
evidence: ShellChange[];
|
|
29
|
+
unjudgeable: ShellUnjudgeable[];
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Derive the file changes one shell command line proves.
|
|
33
|
+
*
|
|
34
|
+
* Never throws: a span the tokenizer could not read is exactly where a quiet pass would
|
|
35
|
+
* hide, so each one answers an unjudgeable entry carrying its reason — and the commands
|
|
36
|
+
* around it still contribute their real evidence.
|
|
37
|
+
*/
|
|
38
|
+
export declare function deriveShellChanges(commandLine: string): ShellDerivation;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell-evidence derivation — one command line in, computed writes plus a reasoned
|
|
3
|
+
* unjudgeable list out.
|
|
4
|
+
*
|
|
5
|
+
* Pure: no disk read, no spawn. `create`/`modify` discrimination and the append composition
|
|
6
|
+
* need a pre-state, which belongs to the judged body; this layer answers only what the
|
|
7
|
+
* command text itself decides.
|
|
8
|
+
*
|
|
9
|
+
* The enumerated forms below ARE the contract — a form none of them names is unjudgeable,
|
|
10
|
+
* never a confident guess. An uncomputable write whose target is known carries that path,
|
|
11
|
+
* so the compiler can route it to the discipline whose scope covers it; a write whose
|
|
12
|
+
* target is unknowable carries none. A command with no mutation signal at all leaves both
|
|
13
|
+
* lists empty: recording a line per read would drown the log it feeds.
|
|
14
|
+
*/
|
|
15
|
+
import { commandNameWord, isNestedShellCommand, tokenizeCommandLine, } from './bash-line.js';
|
|
16
|
+
import { commandBasename, isFdReference, redirectWriteRule, sedInPlaceRule, teeRule, } from './mutation-rules.js';
|
|
17
|
+
import { DEFAULT_READ_ONLY_COMMANDS } from './shell-mod.js';
|
|
18
|
+
// A command that moves the working directory makes every relative target on the line
|
|
19
|
+
// ambiguous — resolving it against a guessed base is exactly the approximation this layer
|
|
20
|
+
// refuses to make.
|
|
21
|
+
const DIRECTORY_CHANGE_COMMANDS = new Set(['cd', 'pushd', 'popd']);
|
|
22
|
+
// The redirect spellings that carry stdout — the only stream whose bytes this layer can
|
|
23
|
+
// compute. Every member is load-bearing: `1>` IS `>` by fd, and `>|`/`1>|` are write
|
|
24
|
+
// operators the tokenizer emits and the detection rules grade by the `>` they contain.
|
|
25
|
+
// Dropping any of them files a computable write as `does not carry stdout`, so the evidence
|
|
26
|
+
// axis would refuse a write the judging axis already accepted.
|
|
27
|
+
const STDOUT_WRITE_OPERATORS = new Set(['>', '>>', '>|', '1>', '1>>', '1>|']);
|
|
28
|
+
// Rules that detect a write with no redirect. The redirect rule is consulted separately,
|
|
29
|
+
// for its fd-reference boundary (`2>&1` is neither a write nor a signal).
|
|
30
|
+
const DETECTION_RULES = [teeRule, sedInPlaceRule];
|
|
31
|
+
/** True when the command's leading words match a read-only allowlist entry's sequence. */
|
|
32
|
+
function isReadOnlyHead(command) {
|
|
33
|
+
return DEFAULT_READ_ONLY_COMMANDS.some((entry) => entry.split(' ').every((entryWord, k) => {
|
|
34
|
+
const word = command.words[k];
|
|
35
|
+
if (word === undefined || word.opaque)
|
|
36
|
+
return false;
|
|
37
|
+
// The first word is compared by basename (`/bin/cat` is still `cat`), later ones verbatim.
|
|
38
|
+
return (k === 0 ? commandBasename(word) : word.text) === entryWord;
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The write-direction redirects of a command. The redirect rule owns the fd-reference
|
|
43
|
+
* boundary, so its detections are the SSOT for what counts as a write; an opaque target
|
|
44
|
+
* keeps its redirect (the rule stays silent on unknowable values, but the write signal
|
|
45
|
+
* is real).
|
|
46
|
+
*/
|
|
47
|
+
function writeRedirects(command) {
|
|
48
|
+
const detected = new Set(redirectWriteRule.detect(command).map((target) => target.path));
|
|
49
|
+
return command.redirects.filter((redirect) => redirect.operator.includes('>') &&
|
|
50
|
+
// An fd duplication is never a write, even when its digit equals another write's
|
|
51
|
+
// filename (`> 1 2>&1`) — without this the digit re-admits it via `detected`.
|
|
52
|
+
!(redirect.operator.endsWith('>&') && isFdReference(redirect.target.text)) &&
|
|
53
|
+
(redirect.target.opaque || detected.has(redirect.target.text)));
|
|
54
|
+
}
|
|
55
|
+
/** The path a target text names, or the reason this layer cannot know it. */
|
|
56
|
+
function resolveTarget(text, opaque, movesDirectory) {
|
|
57
|
+
if (opaque)
|
|
58
|
+
return { reason: `write target ${text} is opaque` };
|
|
59
|
+
// `~` is never expanded here — the same contract the path-notation judge keeps.
|
|
60
|
+
if (text.startsWith('~'))
|
|
61
|
+
return { reason: `write target ${text} is home-relative` };
|
|
62
|
+
if (movesDirectory && !text.startsWith('/')) {
|
|
63
|
+
return { reason: `relative write target ${text} beside a directory change` };
|
|
64
|
+
}
|
|
65
|
+
return { path: text };
|
|
66
|
+
}
|
|
67
|
+
/** File an unjudgeable entry for a target, carrying its path only when one is known. */
|
|
68
|
+
function fileTarget(target, reason) {
|
|
69
|
+
return 'path' in target ? { path: target.path, reason } : { reason: target.reason };
|
|
70
|
+
}
|
|
71
|
+
/** The bytes an `echo` writes: its arguments joined, plus the newline echo appends. */
|
|
72
|
+
function echoContent(command) {
|
|
73
|
+
const args = command.words.slice(1);
|
|
74
|
+
// Flag semantics (`-n`, `-e`) are not computed — the first argument decides, so a later
|
|
75
|
+
// dash-argument is ordinary content.
|
|
76
|
+
if (args[0]?.text.startsWith('-'))
|
|
77
|
+
return { reason: 'echo flag semantics are not computed' };
|
|
78
|
+
if (args.some((word) => word.opaque))
|
|
79
|
+
return { reason: 'an echo argument is opaque' };
|
|
80
|
+
return { content: `${args.map((word) => word.text).join(' ')}\n` };
|
|
81
|
+
}
|
|
82
|
+
/** The bytes a command copying its stdin writes (heredoc, herestring, or a file read). */
|
|
83
|
+
function stdinContent(command, reads) {
|
|
84
|
+
const first = command.words[0];
|
|
85
|
+
// Only a bare `cat` copies stdin to the redirect verbatim; anything else transforms it.
|
|
86
|
+
if (first === undefined || first.opaque || command.words.length > 1) {
|
|
87
|
+
return { reason: 'stdin is transformed by the command, not copied verbatim' };
|
|
88
|
+
}
|
|
89
|
+
if (commandBasename(first) !== 'cat') {
|
|
90
|
+
return { reason: `stdin of ${first.text} is not copied verbatim` };
|
|
91
|
+
}
|
|
92
|
+
const heredocs = command.heredocs ?? [];
|
|
93
|
+
if (heredocs.length + reads.length > 1)
|
|
94
|
+
return { reason: 'more than one stdin source' };
|
|
95
|
+
const heredoc = heredocs[0];
|
|
96
|
+
if (heredoc !== undefined) {
|
|
97
|
+
// Backslash joins the escape set: in an unquoted heredoc bash processes `\` too
|
|
98
|
+
// (a trailing one is a line continuation), so captured bytes would be fiction.
|
|
99
|
+
if (!heredoc.literal && /[$`\\]/.test(heredoc.body)) {
|
|
100
|
+
return { reason: 'an unquoted heredoc body carries an expansion or escape' };
|
|
101
|
+
}
|
|
102
|
+
return { content: heredoc.body };
|
|
103
|
+
}
|
|
104
|
+
const read = reads[0];
|
|
105
|
+
if (read === undefined)
|
|
106
|
+
return { reason: 'no stdin source to read' };
|
|
107
|
+
if (read.operator !== '<<<')
|
|
108
|
+
return { reason: `stdin comes from file ${read.target.text}` };
|
|
109
|
+
if (read.target.opaque)
|
|
110
|
+
return { reason: 'the herestring is opaque' };
|
|
111
|
+
return { content: `${read.target.text}\n` };
|
|
112
|
+
}
|
|
113
|
+
/** The bytes one command writes to its stdout redirect. */
|
|
114
|
+
function computeContent(command) {
|
|
115
|
+
// With no command word the redirection still runs and nothing writes into it.
|
|
116
|
+
if (command.words.length === 0)
|
|
117
|
+
return { content: '' };
|
|
118
|
+
const reads = command.redirects.filter((redirect) => !redirect.operator.includes('>'));
|
|
119
|
+
if (reads.length > 0 || (command.heredocs?.length ?? 0) > 0) {
|
|
120
|
+
return stdinContent(command, reads);
|
|
121
|
+
}
|
|
122
|
+
const first = command.words[0];
|
|
123
|
+
if (first === undefined || first.opaque)
|
|
124
|
+
return { reason: 'the writing command is opaque' };
|
|
125
|
+
if (commandBasename(first) !== 'echo') {
|
|
126
|
+
return { reason: `the output of ${first.text} is not computable` };
|
|
127
|
+
}
|
|
128
|
+
return echoContent(command);
|
|
129
|
+
}
|
|
130
|
+
/** Derive one command's write redirects into evidence or unjudgeable entries. */
|
|
131
|
+
function deriveWrites(command, writes, movesDirectory, derivation) {
|
|
132
|
+
// Two writes on one command mean two results, and computing either alone is wrong.
|
|
133
|
+
if (writes.length > 1) {
|
|
134
|
+
for (const redirect of writes) {
|
|
135
|
+
const target = resolveTarget(redirect.target.text, redirect.target.opaque, movesDirectory);
|
|
136
|
+
derivation.unjudgeable.push(fileTarget(target, 'the command carries more than one write'));
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const redirect = writes[0];
|
|
141
|
+
if (redirect === undefined)
|
|
142
|
+
return;
|
|
143
|
+
const target = resolveTarget(redirect.target.text, redirect.target.opaque, movesDirectory);
|
|
144
|
+
if (!('path' in target)) {
|
|
145
|
+
derivation.unjudgeable.push({ reason: target.reason });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Only stdout carries the content this layer can compute; `2>`/`&>` carry streams it cannot.
|
|
149
|
+
if (!STDOUT_WRITE_OPERATORS.has(redirect.operator)) {
|
|
150
|
+
derivation.unjudgeable.push({
|
|
151
|
+
path: target.path,
|
|
152
|
+
reason: `redirect ${redirect.operator} does not carry stdout`,
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const content = computeContent(command);
|
|
157
|
+
if ('reason' in content) {
|
|
158
|
+
derivation.unjudgeable.push({ path: target.path, reason: content.reason });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
derivation.evidence.push({
|
|
162
|
+
path: target.path,
|
|
163
|
+
content: content.content,
|
|
164
|
+
mode: redirect.operator.endsWith('>>') ? 'append' : 'truncate',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* True when a word carries subshell group syntax. `(`/`)` are ordinary word characters
|
|
169
|
+
* to this tokenizer, so the grouping — and any `cd` inside it — is structurally
|
|
170
|
+
* invisible: a reinterpretation boundary this layer refuses to parse into.
|
|
171
|
+
*/
|
|
172
|
+
function hasSubshellMarker(command) {
|
|
173
|
+
return command.words.some((word) => word.text.startsWith('(') || word.text.endsWith(')'));
|
|
174
|
+
}
|
|
175
|
+
/** Derive one simple command, top to bottom — the first matching case answers. */
|
|
176
|
+
function deriveCommand(command, movesDirectory, derivation) {
|
|
177
|
+
const first = command.words[0];
|
|
178
|
+
// A nested shell re-parses its arguments: a reinterpretation boundary, never parsed into.
|
|
179
|
+
// Read past any leading assignment, or `FOO=1 bash -c …` files nothing at all.
|
|
180
|
+
const name = commandNameWord(command);
|
|
181
|
+
if (name !== undefined && isNestedShellCommand(commandBasename(name))) {
|
|
182
|
+
derivation.unjudgeable.push({ reason: `nested shell execution: ${name.text}` });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (hasSubshellMarker(command)) {
|
|
186
|
+
const grouped = writeRedirects(command);
|
|
187
|
+
if (grouped.length === 0) {
|
|
188
|
+
derivation.unjudgeable.push({ reason: 'subshell group syntax is not parsed here' });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
for (const redirect of grouped) {
|
|
192
|
+
const target = resolveTarget(redirect.target.text, redirect.target.opaque, movesDirectory);
|
|
193
|
+
derivation.unjudgeable.push(fileTarget(target, 'subshell group syntax is not parsed here'));
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// A write redirect and a rule-detected write can ride ONE command (`sed -i f > log`,
|
|
198
|
+
// `… | tee f > /dev/null`) — each files its own row; an early return on either side
|
|
199
|
+
// would swallow the other.
|
|
200
|
+
const writes = writeRedirects(command);
|
|
201
|
+
deriveWrites(command, writes, movesDirectory, derivation);
|
|
202
|
+
const detected = DETECTION_RULES.flatMap((rule) => rule.detect(command));
|
|
203
|
+
for (const mutation of detected) {
|
|
204
|
+
const target = resolveTarget(mutation.path, false, movesDirectory);
|
|
205
|
+
derivation.unjudgeable.push(fileTarget(target, `${mutation.rule} writes content this layer does not compute`));
|
|
206
|
+
}
|
|
207
|
+
if (writes.length > 0 || detected.length > 0)
|
|
208
|
+
return;
|
|
209
|
+
// Nothing detected, but an opaque token under a command that is not proven read-only
|
|
210
|
+
// could still write — the signal remains even where the rules stay silent. Without that
|
|
211
|
+
// allowlist gate every read over a glob would file a line (the volume defence).
|
|
212
|
+
if (command.words.some((word) => word.opaque) && !isReadOnlyHead(command)) {
|
|
213
|
+
derivation.unjudgeable.push({
|
|
214
|
+
reason: `opaque token under ${first?.text ?? 'a command'} that is not proven read-only`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Derive the file changes one shell command line proves.
|
|
220
|
+
*
|
|
221
|
+
* Never throws: a span the tokenizer could not read is exactly where a quiet pass would
|
|
222
|
+
* hide, so each one answers an unjudgeable entry carrying its reason — and the commands
|
|
223
|
+
* around it still contribute their real evidence.
|
|
224
|
+
*/
|
|
225
|
+
export function deriveShellChanges(commandLine) {
|
|
226
|
+
const result = tokenizeCommandLine(commandLine);
|
|
227
|
+
// A directory change is line-scoped and order-blind: a write before it is as ambiguous
|
|
228
|
+
// as one after, since only execution decides which base each relative target resolves to.
|
|
229
|
+
const movesDirectory = result.commands.some((command) => {
|
|
230
|
+
// Read past leading assignments, like `deriveCommand` — `FOO=1 cd sub` moves the directory
|
|
231
|
+
// exactly as `cd sub` does. Missing it is not a silent skip: every relative target on
|
|
232
|
+
// the line would then be resolved against the repo root and filed as CONFIDENT evidence
|
|
233
|
+
// for a path the command never touched.
|
|
234
|
+
const name = commandNameWord(command);
|
|
235
|
+
return (name !== undefined && !name.opaque && DIRECTORY_CHANGE_COMMANDS.has(commandBasename(name)));
|
|
236
|
+
});
|
|
237
|
+
// A line with any unread span files at least one entry, whatever the commands around it
|
|
238
|
+
// derive: that row is the telemetry the shell axis contracts for, and a partial success
|
|
239
|
+
// that swallowed it would be a call passing unrecorded.
|
|
240
|
+
const derivation = {
|
|
241
|
+
evidence: [],
|
|
242
|
+
unjudgeable: result.unread.map((span) => ({ reason: span.reason })),
|
|
243
|
+
};
|
|
244
|
+
for (const command of result.commands)
|
|
245
|
+
deriveCommand(command, movesDirectory, derivation);
|
|
246
|
+
return derivation;
|
|
247
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `judgeShellModification` — the shell-mod meta-covenant's pure judge (zero I/O).
|
|
3
|
+
*
|
|
4
|
+
* Analyzes the command-line strings of *shell* tool calls (names and arg keys are injected
|
|
5
|
+
* values, never source literals) per simple command: the fixed detection rules catch writes
|
|
6
|
+
* to a protected path, undecidable structures (opaque mentions, opaque write targets) fail
|
|
7
|
+
* closed, the read-only allowlist absolves proven reads, and every other protected-path
|
|
8
|
+
* mention breaks — "mention + unproven = block". It judges only its own axis: a non-shell
|
|
9
|
+
* tool call is upheld, since the tool axis belongs to the self-mod meta-covenant and
|
|
10
|
+
* run-all co-existence depends on that boundary.
|
|
11
|
+
*/
|
|
12
|
+
import type { CovenantInput, CovenantVerdict } from '@polydeukes/core';
|
|
13
|
+
import { type SimpleCommand } from './bash-line.ts';
|
|
14
|
+
import type { CovenantRegistration, MetaCovenantRegistration } from './dispatch.ts';
|
|
15
|
+
/**
|
|
16
|
+
* `ShellModificationSpec` — the injected axes of the judge.
|
|
17
|
+
*
|
|
18
|
+
* `protectedPaths` are literal path strings; `shellToolNames` are the tool names whose
|
|
19
|
+
* calls carry shell lines; `commandArgNames` are the `args` keys those lines live under;
|
|
20
|
+
* `readOnlyCommands` are allowlist entries — space-separated word sequences (`'cat'`,
|
|
21
|
+
* `'git diff'`). Empty-string entries in every list are ignored (an unguarded `''` would
|
|
22
|
+
* match every path / tool / arg / command).
|
|
23
|
+
*/
|
|
24
|
+
export type ShellModificationSpec = {
|
|
25
|
+
protectedPaths: string[];
|
|
26
|
+
shellToolNames: string[];
|
|
27
|
+
commandArgNames: string[];
|
|
28
|
+
readOnlyCommands: string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Commands proven read-only by shell semantics — the default allowlist. An entry is a
|
|
32
|
+
* leading word sequence; multi-word entries exist because a bare command name (`git`) can
|
|
33
|
+
* front mutating subcommands. Omission errs toward friction, never a hole.
|
|
34
|
+
*
|
|
35
|
+
* An entry must have no way to write a file through its own arguments, since the allowlist
|
|
36
|
+
* vouches for the command head while `matchesReadOnlyEntry` never inspects trailing argv.
|
|
37
|
+
* That is why `git diff`/`git log`/`git show` are absent: all accept `--output=<file>`, a
|
|
38
|
+
* redirect-free truncating write. `git status`/`git grep` reject `--output`, so they stay.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DEFAULT_READ_ONLY_COMMANDS: string[];
|
|
41
|
+
/**
|
|
42
|
+
* True when the command's leading words match the allowlist entry's word sequence. Exported
|
|
43
|
+
* so the transcript judge's allowlist clause absolves reads by this exact comparison instead
|
|
44
|
+
* of a fork that could drift from it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function matchesReadOnlyEntry(command: SimpleCommand, entry: string[]): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Judge a {@link CovenantInput} against the shell-mod spec (pure).
|
|
49
|
+
*
|
|
50
|
+
* For each `toolCalls[i]` whose `name` exactly equals a non-empty `shellToolNames` entry,
|
|
51
|
+
* every string value under a non-empty `commandArgNames` key is analyzed as a shell line;
|
|
52
|
+
* a shell call with zero such strings breaks (a misassembled arg name must not degrade
|
|
53
|
+
* into universal uphold). A span the tokenizer could not read breaks iff the dequoted span —
|
|
54
|
+
* or one of its shell-metacharacter fragments — mentions a protected path, and is answered
|
|
55
|
+
* before the commands so that a mention only the span can see is named as one. Non-shell
|
|
56
|
+
* calls, `subagentSpawns`, and `userMessages` are never judged.
|
|
57
|
+
*/
|
|
58
|
+
export declare function judgeShellModification(input: CovenantInput, spec: ShellModificationSpec): CovenantVerdict;
|
|
59
|
+
/**
|
|
60
|
+
* `ShellModRegistrationSpec` — the assembly values baked into the registration. The call
|
|
61
|
+
* set is not among them: the dispatcher supplies it to the judge at call time.
|
|
62
|
+
* `readOnlyCommands` REPLACES {@link DEFAULT_READ_ONLY_COMMANDS} when given — no merge,
|
|
63
|
+
* since an assembly wanting to extend the default spreads the constant.
|
|
64
|
+
*/
|
|
65
|
+
export type ShellModRegistrationSpec = {
|
|
66
|
+
protectedPaths: string[];
|
|
67
|
+
shellTools: string[];
|
|
68
|
+
/**
|
|
69
|
+
* The `args` keys shell lines live under. ABSENT is the empty surface, which the thunk's
|
|
70
|
+
* entry gate refuses — naming no command arg would otherwise uphold every call.
|
|
71
|
+
*/
|
|
72
|
+
commandArgs?: string[];
|
|
73
|
+
readOnlyCommands?: string[];
|
|
74
|
+
witness?: CovenantRegistration['witness'];
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Build the shell-mod registration. Routing stays path mention; the judgment is the thunk.
|
|
78
|
+
*
|
|
79
|
+
* The misassembly gate lives at the thunk's entry: zero valid entries in any of the three
|
|
80
|
+
* required lists would make {@link judgeShellModification} uphold every call, so it answers
|
|
81
|
+
* the unjudgeable outcome instead, which no enforce level softens. The allowlist is exempt
|
|
82
|
+
* — empty just means stricter.
|
|
83
|
+
*/
|
|
84
|
+
export declare function shellModRegistration(spec: ShellModRegistrationSpec): MetaCovenantRegistration;
|