crewhaus 0.1.8 → 0.2.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 +10 -3
- package/dist/advice-apply.d.ts +182 -0
- package/dist/advice-apply.js +286 -0
- package/dist/advise-rules.d.ts +348 -0
- package/dist/advise-rules.js +905 -0
- package/dist/alert-sink.d.ts +48 -0
- package/dist/alert-sink.js +86 -0
- package/dist/approval-gate.d.ts +127 -0
- package/dist/approval-gate.js +254 -0
- package/dist/audit-verify.d.ts +69 -0
- package/dist/audit-verify.js +97 -0
- package/dist/autodistill.d.ts +113 -0
- package/dist/autodistill.js +256 -0
- package/dist/channel-provision.d.ts +360 -0
- package/dist/channel-provision.js +881 -0
- package/dist/ci-scaffold.d.ts +31 -0
- package/dist/ci-scaffold.js +343 -0
- package/dist/compile-check.d.ts +90 -0
- package/dist/compile-check.js +285 -0
- package/dist/compliance-schedule.d.ts +35 -0
- package/dist/compliance-schedule.js +36 -0
- package/dist/context-pressure.d.ts +80 -0
- package/dist/context-pressure.js +166 -0
- package/dist/dataset-mine.d.ts +173 -0
- package/dist/dataset-mine.js +404 -0
- package/dist/datasets.d.ts +124 -0
- package/dist/datasets.js +260 -0
- package/dist/deploy-canary.d.ts +83 -0
- package/dist/deploy-canary.js +87 -0
- package/dist/doctor-checks.d.ts +33 -0
- package/dist/doctor-checks.js +92 -0
- package/dist/doctor-detect.d.ts +108 -0
- package/dist/doctor-detect.js +214 -0
- package/dist/doctor-fix.d.ts +81 -0
- package/dist/doctor-fix.js +164 -0
- package/dist/egress-triage.d.ts +121 -0
- package/dist/egress-triage.js +261 -0
- package/dist/eval-bridge.d.ts +114 -0
- package/dist/eval-bridge.js +158 -0
- package/dist/eval-coverage.d.ts +140 -0
- package/dist/eval-coverage.js +428 -0
- package/dist/eval-history.d.ts +48 -0
- package/dist/eval-history.js +157 -0
- package/dist/eval-matrix.d.ts +80 -0
- package/dist/eval-matrix.js +182 -0
- package/dist/eval-sentinel.d.ts +65 -0
- package/dist/eval-sentinel.js +132 -0
- package/dist/faq.d.ts +68 -0
- package/dist/faq.js +168 -0
- package/dist/feedback.d.ts +9 -2
- package/dist/feedback.js +17 -7
- package/dist/fewshot.d.ts +83 -0
- package/dist/fewshot.js +158 -0
- package/dist/fleet.d.ts +207 -0
- package/dist/fleet.js +488 -0
- package/dist/flywheel.d.ts +193 -0
- package/dist/flywheel.js +519 -0
- package/dist/graders-suggest.d.ts +186 -0
- package/dist/graders-suggest.js +658 -0
- package/dist/incident.d.ts +99 -0
- package/dist/incident.js +217 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +11664 -963
- package/dist/init-interactive.d.ts +105 -0
- package/dist/init-interactive.js +208 -0
- package/dist/intents.d.ts +105 -0
- package/dist/intents.js +292 -0
- package/dist/judge-calibrate.d.ts +137 -0
- package/dist/judge-calibrate.js +247 -0
- package/dist/justification-calibrate.d.ts +150 -0
- package/dist/justification-calibrate.js +262 -0
- package/dist/justification-gate.d.ts +27 -6
- package/dist/justification-gate.js +30 -6
- package/dist/knowledge-sync.d.ts +179 -0
- package/dist/knowledge-sync.js +551 -0
- package/dist/lessons.d.ts +87 -0
- package/dist/lessons.js +207 -0
- package/dist/lint.d.ts +127 -0
- package/dist/lint.js +226 -0
- package/dist/loadtest.d.ts +114 -0
- package/dist/loadtest.js +196 -0
- package/dist/marketplace-cli.d.ts +110 -0
- package/dist/marketplace-cli.js +250 -0
- package/dist/mcp-doctor.d.ts +121 -0
- package/dist/mcp-doctor.js +249 -0
- package/dist/model-scan.d.ts +116 -0
- package/dist/model-scan.js +226 -0
- package/dist/onchain-tune.d.ts +164 -0
- package/dist/onchain-tune.js +346 -0
- package/dist/permissions-suggest.d.ts +126 -0
- package/dist/permissions-suggest.js +333 -0
- package/dist/pii-tune.d.ts +107 -0
- package/dist/pii-tune.js +122 -0
- package/dist/propose.d.ts +117 -0
- package/dist/propose.js +184 -0
- package/dist/refresh-goldens.d.ts +82 -0
- package/dist/refresh-goldens.js +221 -0
- package/dist/regression-pin.d.ts +160 -0
- package/dist/regression-pin.js +281 -0
- package/dist/retention.d.ts +193 -0
- package/dist/retention.js +607 -0
- package/dist/retire.d.ts +118 -0
- package/dist/retire.js +291 -0
- package/dist/right-size.d.ts +100 -0
- package/dist/right-size.js +123 -0
- package/dist/route.d.ts +19 -0
- package/dist/route.js +90 -0
- package/dist/scaffold-evals.d.ts +138 -0
- package/dist/scaffold-evals.js +410 -0
- package/dist/scope-audit-drift.d.ts +139 -0
- package/dist/scope-audit-drift.js +260 -0
- package/dist/security-corpus.d.ts +237 -0
- package/dist/security-corpus.js +516 -0
- package/dist/security-digest.d.ts +174 -0
- package/dist/security-digest.js +656 -0
- package/dist/sessions-index.d.ts +27 -0
- package/dist/sessions-index.js +51 -0
- package/dist/slo-doctor.d.ts +67 -0
- package/dist/slo-doctor.js +119 -0
- package/dist/slo-sink.d.ts +96 -0
- package/dist/slo-sink.js +107 -0
- package/dist/spec-changelog.d.ts +102 -0
- package/dist/spec-changelog.js +237 -0
- package/dist/state-backup.d.ts +223 -0
- package/dist/state-backup.js +648 -0
- package/dist/tools-cli.d.ts +170 -0
- package/dist/tools-cli.js +300 -0
- package/dist/triage.d.ts +202 -0
- package/dist/triage.js +403 -0
- package/dist/upgrade.d.ts +57 -0
- package/dist/upgrade.js +113 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +27 -0
- package/dist/voice-eval.d.ts +138 -0
- package/dist/voice-eval.js +309 -0
- package/dist/watch.d.ts +58 -0
- package/dist/watch.js +97 -0
- package/package.json +90 -65
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item 66 — `crewhaus onchain tune` / `onchain sentinel`: mine wallet-engine
|
|
3
|
+
* transaction receipts + simulation history to (a) propose a tuned
|
|
4
|
+
* `transaction_policy` from the observed legitimate spend distribution, and
|
|
5
|
+
* (b) flag anomalous spend against a learned baseline (an alert-watchdog for
|
|
6
|
+
* on-chain value).
|
|
7
|
+
*
|
|
8
|
+
* Input is a receipt-history JSONL — one record per broadcast, carrying the
|
|
9
|
+
* fields wallet-engine already surfaces on `BroadcastReceipt` + the tx it
|
|
10
|
+
* signed: `{ ts, walletId, chainId, contractId, valueWei, status, simulated }`.
|
|
11
|
+
* NO private key material is ever read — `keyRef` stays a reference and never
|
|
12
|
+
* enters this module; a receipt carries only a public tx hash, a contract id,
|
|
13
|
+
* and a wei value.
|
|
14
|
+
*
|
|
15
|
+
* `transaction_policy` is optimizer-whitelisted for the onchain / onchain-game
|
|
16
|
+
* targets (OPTIMIZABLE_PATHS), so the tuner emits VALIDATED `SpecPatch`es there
|
|
17
|
+
* (a `crewhaus optimize --write-back`-compatible edit). For a non-whitelisted
|
|
18
|
+
* target the tuner degrades to advice-only (the proposal is printed, no patch).
|
|
19
|
+
*
|
|
20
|
+
* Everything here is side-effect-free (the CLI entry file reads the history
|
|
21
|
+
* JSONL + writes patches/report). Deterministic: same receipts → same proposal.
|
|
22
|
+
*/
|
|
23
|
+
/** Thrown on malformed history / bad flags. The CLI routes it through `die()`;
|
|
24
|
+
* tests assert on `.message`. */
|
|
25
|
+
export declare class OnchainTuneError extends Error {
|
|
26
|
+
readonly name = "OnchainTuneError";
|
|
27
|
+
}
|
|
28
|
+
/** One parsed receipt-history record. `valueWei` is the native-token value the
|
|
29
|
+
* tx moved, as a decimal or 0x-hex string (BigInt-parsed downstream). */
|
|
30
|
+
export type ReceiptRecord = {
|
|
31
|
+
readonly ts: number;
|
|
32
|
+
readonly walletId: string;
|
|
33
|
+
readonly chainId: string;
|
|
34
|
+
/** The spec-level contract binding id the tx targeted (symbolic allowlist). */
|
|
35
|
+
readonly contractId: string;
|
|
36
|
+
readonly valueWei: bigint;
|
|
37
|
+
/** "0x1" success / "0x0" revert / null pending — mirrors BroadcastReceipt. */
|
|
38
|
+
readonly status: "0x1" | "0x0" | null;
|
|
39
|
+
/** Whether a pre-broadcast simulation ran + succeeded. */
|
|
40
|
+
readonly simulated: boolean;
|
|
41
|
+
};
|
|
42
|
+
/** The current transaction_policy (subset the tuner reads/proposes). */
|
|
43
|
+
export type CurrentPolicy = {
|
|
44
|
+
readonly maxValueWei?: string;
|
|
45
|
+
readonly allowedContracts?: readonly string[];
|
|
46
|
+
readonly simulationRequired?: boolean;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Parse a receipt-history JSONL blob. Skips blank lines; throws on a
|
|
50
|
+
* structurally invalid record so a corrupt history is loud. A receipt with no
|
|
51
|
+
* `contractId` (a raw-address send — already forbidden by wallet-engine) is
|
|
52
|
+
* rejected so the tuner never learns an allowlist from un-symbolic sends.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseReceiptHistory(jsonl: string): ReceiptRecord[];
|
|
55
|
+
export type ProposePolicyOptions = {
|
|
56
|
+
/** Headroom multiplier over the observed max legitimate spend for the
|
|
57
|
+
* proposed maxValueWei cap. Default 1.25 (25% headroom). */
|
|
58
|
+
readonly capMarginPct?: number;
|
|
59
|
+
/** Whether the source spec's target has transaction_policy whitelisted for
|
|
60
|
+
* optimizer write-back (onchain / onchain-game). When false, the tuner still
|
|
61
|
+
* computes the proposal but emits no SpecPatch (advice-only). */
|
|
62
|
+
readonly optimizable: boolean;
|
|
63
|
+
/** The spec target (for the SpecPatch discriminator). */
|
|
64
|
+
readonly target: string;
|
|
65
|
+
};
|
|
66
|
+
/** The tuner's proposal: the derived caps + allowlist and, when whitelisted,
|
|
67
|
+
* the validated transaction_policy block to write back. The CLI wraps this in
|
|
68
|
+
* a SpecPatch with the right op (`add` vs `replace`) once it has seen whether
|
|
69
|
+
* the raw YAML already carries a transaction_policy block. */
|
|
70
|
+
export type PolicyProposal = {
|
|
71
|
+
/** Proposed native-token ceiling (wei, decimal string). undefined when no
|
|
72
|
+
* successful spend was observed (nothing to bound). */
|
|
73
|
+
readonly proposedMaxValueWei?: string;
|
|
74
|
+
/** Contract ids observed in successful sends — the proposed allowlist. */
|
|
75
|
+
readonly proposedAllowedContracts: readonly string[];
|
|
76
|
+
/** Human-readable derivation notes. */
|
|
77
|
+
readonly rationale: string;
|
|
78
|
+
/** The transaction_policy block to write back (+ where + why), or undefined
|
|
79
|
+
* for advice-only. `op` is left to the CLI (it depends on raw-YAML presence). */
|
|
80
|
+
readonly patch?: {
|
|
81
|
+
readonly target: string;
|
|
82
|
+
readonly path: readonly string[];
|
|
83
|
+
readonly value: Record<string, unknown>;
|
|
84
|
+
readonly rationale: string;
|
|
85
|
+
};
|
|
86
|
+
/** Warnings the author should see (e.g. current cap is too tight vs observed). */
|
|
87
|
+
readonly warnings: readonly string[];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Propose a tuned transaction_policy from the observed legitimate spend. The cap
|
|
91
|
+
* is `max(successful valueWei) × capMargin`, rounded up; the allowlist is the
|
|
92
|
+
* set of contract ids that produced a successful send. When the target
|
|
93
|
+
* whitelists `transaction_policy` for the optimizer, a validated `replace`
|
|
94
|
+
* SpecPatch on the whole block is returned so `optimize --write-back` can apply
|
|
95
|
+
* it; otherwise the proposal is advice-only.
|
|
96
|
+
*/
|
|
97
|
+
export declare function proposePolicy(receipts: readonly ReceiptRecord[], current: CurrentPolicy, opts: ProposePolicyOptions): PolicyProposal;
|
|
98
|
+
/** Per-contract spend baseline (successful sends only). All in wei. */
|
|
99
|
+
export type SpendBaseline = {
|
|
100
|
+
readonly perContract: Readonly<Record<string, {
|
|
101
|
+
count: number;
|
|
102
|
+
maxWei: bigint;
|
|
103
|
+
meanWei: bigint;
|
|
104
|
+
}>>;
|
|
105
|
+
/** Global ceiling across all contracts (max successful spend). */
|
|
106
|
+
readonly globalMaxWei: bigint;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Learn a spend baseline from successful receipts. Per contract we track count,
|
|
110
|
+
* max, and mean (integer wei). The map key is the raw contractId — contract
|
|
111
|
+
* ids are `[A-Za-z0-9_.-]`-ish spec identifiers, so a `|`-joined composite key
|
|
112
|
+
* is never needed and no control/NUL byte ever enters a key.
|
|
113
|
+
*/
|
|
114
|
+
export declare function learnSpendBaseline(receipts: readonly ReceiptRecord[]): SpendBaseline;
|
|
115
|
+
/** One flagged anomaly. */
|
|
116
|
+
export type SpendAnomaly = {
|
|
117
|
+
readonly ts: number;
|
|
118
|
+
readonly contractId: string;
|
|
119
|
+
readonly valueWei: bigint;
|
|
120
|
+
readonly reason: string;
|
|
121
|
+
};
|
|
122
|
+
export type SentinelOptions = {
|
|
123
|
+
/** How many times over the per-contract observed max counts as anomalous.
|
|
124
|
+
* Default 2 (a spend > 2× the largest previously-seen legit send). */
|
|
125
|
+
readonly maxMultiple?: number;
|
|
126
|
+
/** Also flag a send to a contract id never seen in the baseline. Default true. */
|
|
127
|
+
readonly flagUnknownContracts?: boolean;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Detect anomalous spend in `candidate` receipts against a learned baseline.
|
|
131
|
+
* Flags: (a) a send to a contract id absent from the baseline, and (b) a value
|
|
132
|
+
* exceeding `maxMultiple ×` that contract's observed max. Deterministic; the
|
|
133
|
+
* baseline itself is never mutated. keyRef / private keys never enter here.
|
|
134
|
+
*/
|
|
135
|
+
export declare function detectAnomalies(candidate: readonly ReceiptRecord[], baseline: SpendBaseline, opts?: SentinelOptions): SpendAnomaly[];
|
|
136
|
+
/**
|
|
137
|
+
* Detect anomalous spend when candidate and baseline are the SAME history
|
|
138
|
+
* (`crewhaus onchain sentinel` invoked without `--baseline`) — the "self-
|
|
139
|
+
* compare" mode.
|
|
140
|
+
*
|
|
141
|
+
* A naive `learnSpendBaseline(history)` + `detectAnomalies(history, ...)`
|
|
142
|
+
* self-masks: a receipt's own value is folded into its contract's baseline
|
|
143
|
+
* max, so a lone 100× spike can never exceed `maxMultiple × (its own value)`.
|
|
144
|
+
* The value-ceiling check — the headline sentinel feature — is silently
|
|
145
|
+
* defeated in exactly the default (no-`--baseline`) invocation.
|
|
146
|
+
*
|
|
147
|
+
* Fix: leave-one-out. For each candidate receipt, the ceiling is computed
|
|
148
|
+
* from the max successful spend over every OTHER successful receipt for that
|
|
149
|
+
* contract — the receipt under test never contributes to its own ceiling. A
|
|
150
|
+
* lone spike is then measured against the normal population and correctly
|
|
151
|
+
* flagged; a normal history (every value within range of its peers) still
|
|
152
|
+
* flags nothing. Unknown-contract detection needs no leave-one-out (a
|
|
153
|
+
* contract either has ≥1 OTHER successful receipt or it doesn't — a receipt
|
|
154
|
+
* is never "unknown relative to itself" in a way that matters here, since a
|
|
155
|
+
* lone successful send to a contract is, by definition, its own baseline of
|
|
156
|
+
* one and cannot be a value anomaly against itself); this path only ever
|
|
157
|
+
* calls detectAnomalies-style value/unknown logic per receipt with a leave-
|
|
158
|
+
* one-out baseline substituted in.
|
|
159
|
+
*/
|
|
160
|
+
export declare function detectAnomaliesSelfCompare(history: readonly ReceiptRecord[], opts?: SentinelOptions): SpendAnomaly[];
|
|
161
|
+
/** Render a plain-text tune report. */
|
|
162
|
+
export declare function renderTuneReport(proposal: PolicyProposal): string;
|
|
163
|
+
/** Render a plain-text sentinel report. */
|
|
164
|
+
export declare function renderSentinelReport(anomalies: readonly SpendAnomaly[]): string;
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item 66 — `crewhaus onchain tune` / `onchain sentinel`: mine wallet-engine
|
|
3
|
+
* transaction receipts + simulation history to (a) propose a tuned
|
|
4
|
+
* `transaction_policy` from the observed legitimate spend distribution, and
|
|
5
|
+
* (b) flag anomalous spend against a learned baseline (an alert-watchdog for
|
|
6
|
+
* on-chain value).
|
|
7
|
+
*
|
|
8
|
+
* Input is a receipt-history JSONL — one record per broadcast, carrying the
|
|
9
|
+
* fields wallet-engine already surfaces on `BroadcastReceipt` + the tx it
|
|
10
|
+
* signed: `{ ts, walletId, chainId, contractId, valueWei, status, simulated }`.
|
|
11
|
+
* NO private key material is ever read — `keyRef` stays a reference and never
|
|
12
|
+
* enters this module; a receipt carries only a public tx hash, a contract id,
|
|
13
|
+
* and a wei value.
|
|
14
|
+
*
|
|
15
|
+
* `transaction_policy` is optimizer-whitelisted for the onchain / onchain-game
|
|
16
|
+
* targets (OPTIMIZABLE_PATHS), so the tuner emits VALIDATED `SpecPatch`es there
|
|
17
|
+
* (a `crewhaus optimize --write-back`-compatible edit). For a non-whitelisted
|
|
18
|
+
* target the tuner degrades to advice-only (the proposal is printed, no patch).
|
|
19
|
+
*
|
|
20
|
+
* Everything here is side-effect-free (the CLI entry file reads the history
|
|
21
|
+
* JSONL + writes patches/report). Deterministic: same receipts → same proposal.
|
|
22
|
+
*/
|
|
23
|
+
/** Thrown on malformed history / bad flags. The CLI routes it through `die()`;
|
|
24
|
+
* tests assert on `.message`. */
|
|
25
|
+
export class OnchainTuneError extends Error {
|
|
26
|
+
name = "OnchainTuneError";
|
|
27
|
+
}
|
|
28
|
+
function parseWeiString(raw, ctx) {
|
|
29
|
+
if (typeof raw === "number" && Number.isInteger(raw) && raw >= 0)
|
|
30
|
+
return BigInt(raw);
|
|
31
|
+
if (typeof raw === "string" && /^(0x[0-9a-fA-F]+|[0-9]+)$/.test(raw.trim())) {
|
|
32
|
+
return BigInt(raw.trim());
|
|
33
|
+
}
|
|
34
|
+
throw new OnchainTuneError(`${ctx}: valueWei must be a non-negative wei decimal or 0x-hex string`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse a receipt-history JSONL blob. Skips blank lines; throws on a
|
|
38
|
+
* structurally invalid record so a corrupt history is loud. A receipt with no
|
|
39
|
+
* `contractId` (a raw-address send — already forbidden by wallet-engine) is
|
|
40
|
+
* rejected so the tuner never learns an allowlist from un-symbolic sends.
|
|
41
|
+
*/
|
|
42
|
+
export function parseReceiptHistory(jsonl) {
|
|
43
|
+
const out = [];
|
|
44
|
+
const lines = jsonl.split("\n");
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
const line = lines[i];
|
|
47
|
+
if (line === undefined || line.trim() === "")
|
|
48
|
+
continue;
|
|
49
|
+
let obj;
|
|
50
|
+
try {
|
|
51
|
+
obj = JSON.parse(line);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new OnchainTuneError(`receipt history line ${i + 1}: not valid JSON`);
|
|
55
|
+
}
|
|
56
|
+
if (obj === null || typeof obj !== "object") {
|
|
57
|
+
throw new OnchainTuneError(`receipt history line ${i + 1}: not an object`);
|
|
58
|
+
}
|
|
59
|
+
const rec = obj;
|
|
60
|
+
const contractId = rec["contractId"];
|
|
61
|
+
if (typeof contractId !== "string" || contractId.trim() === "") {
|
|
62
|
+
throw new OnchainTuneError(`receipt history line ${i + 1}: missing contractId (raw-address sends are not tunable)`);
|
|
63
|
+
}
|
|
64
|
+
const ts = typeof rec["ts"] === "number" ? rec["ts"] : i;
|
|
65
|
+
const walletId = typeof rec["walletId"] === "string" ? rec["walletId"] : "wallet";
|
|
66
|
+
const chainId = typeof rec["chainId"] === "string" ? rec["chainId"] : "chain";
|
|
67
|
+
const valueWei = parseWeiString(rec["valueWei"] ?? "0", `receipt history line ${i + 1}`);
|
|
68
|
+
const statusRaw = rec["status"];
|
|
69
|
+
const status = statusRaw === "0x1" || statusRaw === "0x0" ? statusRaw : null;
|
|
70
|
+
const simulated = rec["simulated"] === true;
|
|
71
|
+
out.push({ ts, walletId, chainId, contractId, valueWei, status, simulated });
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** Only receipts that actually settled successfully count as "legitimate" spend
|
|
76
|
+
* the policy should permit. Reverts + pending are excluded from the baseline. */
|
|
77
|
+
function successfulReceipts(receipts) {
|
|
78
|
+
return receipts.filter((r) => r.status === "0x1");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Propose a tuned transaction_policy from the observed legitimate spend. The cap
|
|
82
|
+
* is `max(successful valueWei) × capMargin`, rounded up; the allowlist is the
|
|
83
|
+
* set of contract ids that produced a successful send. When the target
|
|
84
|
+
* whitelists `transaction_policy` for the optimizer, a validated `replace`
|
|
85
|
+
* SpecPatch on the whole block is returned so `optimize --write-back` can apply
|
|
86
|
+
* it; otherwise the proposal is advice-only.
|
|
87
|
+
*/
|
|
88
|
+
export function proposePolicy(receipts, current, opts) {
|
|
89
|
+
const margin = opts.capMarginPct ?? 1.25;
|
|
90
|
+
if (margin <= 1) {
|
|
91
|
+
throw new OnchainTuneError(`capMarginPct must be > 1 (got ${margin})`);
|
|
92
|
+
}
|
|
93
|
+
const ok = successfulReceipts(receipts);
|
|
94
|
+
const warnings = [];
|
|
95
|
+
// Allowlist = contract ids with ≥1 successful send, sorted + deduped.
|
|
96
|
+
const allowed = [...new Set(ok.map((r) => r.contractId))].sort();
|
|
97
|
+
// Cap = ceil(maxObservedWei * margin). BigInt-safe: scale the margin into an
|
|
98
|
+
// integer numerator/denominator to avoid float wei.
|
|
99
|
+
let proposedMaxValueWei;
|
|
100
|
+
let capRationale = "no successful sends observed — cannot bound spend";
|
|
101
|
+
if (ok.length > 0) {
|
|
102
|
+
let maxWei = 0n;
|
|
103
|
+
for (const r of ok)
|
|
104
|
+
if (r.valueWei > maxWei)
|
|
105
|
+
maxWei = r.valueWei;
|
|
106
|
+
// margin as basis points to stay integer: e.g. 1.25 → 12500 / 10000.
|
|
107
|
+
const bps = BigInt(Math.round(margin * 10_000));
|
|
108
|
+
const cap = (maxWei * bps + 9999n) / 10000n; // ceil-div
|
|
109
|
+
proposedMaxValueWei = cap.toString();
|
|
110
|
+
capRationale = `max observed successful spend ${maxWei} wei × ${margin} = ${cap} wei cap`;
|
|
111
|
+
// If the current cap is BELOW the observed legitimate max, flag it: real
|
|
112
|
+
// spend is being denied. If it's absent, note the newly-derived ceiling.
|
|
113
|
+
if (current.maxValueWei !== undefined) {
|
|
114
|
+
const curCap = parseWeiString(current.maxValueWei, "current transaction_policy.maxValueWei");
|
|
115
|
+
if (curCap < maxWei) {
|
|
116
|
+
warnings.push(`current maxValueWei ${curCap} wei is BELOW observed legitimate spend ${maxWei} wei — legitimate sends are being denied`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Contract ids that appear in the CURRENT allowlist but never in a successful
|
|
121
|
+
// send are candidates for removal (least-privilege). Advisory only.
|
|
122
|
+
if (current.allowedContracts !== undefined) {
|
|
123
|
+
const unused = current.allowedContracts.filter((c) => !allowed.includes(c));
|
|
124
|
+
if (unused.length > 0) {
|
|
125
|
+
warnings.push(`current allowedContracts includes never-used ids ${JSON.stringify(unused)} — consider removing (least privilege)`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const excludedNote = ok.length < receipts.length
|
|
129
|
+
? ` (${receipts.length - ok.length} reverted/pending receipts excluded)`
|
|
130
|
+
: "";
|
|
131
|
+
const rationale = `${capRationale}; allowlist derived from ${allowed.length} contract id(s) with successful sends${excludedNote}`;
|
|
132
|
+
const proposal = {
|
|
133
|
+
...(proposedMaxValueWei !== undefined ? { proposedMaxValueWei } : {}),
|
|
134
|
+
proposedAllowedContracts: allowed,
|
|
135
|
+
rationale,
|
|
136
|
+
warnings,
|
|
137
|
+
};
|
|
138
|
+
if (opts.optimizable && proposedMaxValueWei !== undefined) {
|
|
139
|
+
// Whole-block replace so the write-back preserves the other policy fields.
|
|
140
|
+
const value = {
|
|
141
|
+
...(current.simulationRequired !== undefined
|
|
142
|
+
? { simulationRequired: current.simulationRequired }
|
|
143
|
+
: { simulationRequired: true }),
|
|
144
|
+
maxValueWei: proposedMaxValueWei,
|
|
145
|
+
allowedContracts: allowed,
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
...proposal,
|
|
149
|
+
patch: {
|
|
150
|
+
target: opts.target,
|
|
151
|
+
path: ["transaction_policy"],
|
|
152
|
+
value,
|
|
153
|
+
rationale: `onchain tune: ${rationale}`,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return proposal;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Learn a spend baseline from successful receipts. Per contract we track count,
|
|
161
|
+
* max, and mean (integer wei). The map key is the raw contractId — contract
|
|
162
|
+
* ids are `[A-Za-z0-9_.-]`-ish spec identifiers, so a `|`-joined composite key
|
|
163
|
+
* is never needed and no control/NUL byte ever enters a key.
|
|
164
|
+
*/
|
|
165
|
+
export function learnSpendBaseline(receipts) {
|
|
166
|
+
const ok = successfulReceipts(receipts);
|
|
167
|
+
const acc = new Map();
|
|
168
|
+
let globalMaxWei = 0n;
|
|
169
|
+
for (const r of ok) {
|
|
170
|
+
const cur = acc.get(r.contractId) ?? { count: 0, maxWei: 0n, sumWei: 0n };
|
|
171
|
+
cur.count += 1;
|
|
172
|
+
cur.sumWei += r.valueWei;
|
|
173
|
+
if (r.valueWei > cur.maxWei)
|
|
174
|
+
cur.maxWei = r.valueWei;
|
|
175
|
+
acc.set(r.contractId, cur);
|
|
176
|
+
if (r.valueWei > globalMaxWei)
|
|
177
|
+
globalMaxWei = r.valueWei;
|
|
178
|
+
}
|
|
179
|
+
const perContract = {};
|
|
180
|
+
for (const [id, v] of acc) {
|
|
181
|
+
perContract[id] = {
|
|
182
|
+
count: v.count,
|
|
183
|
+
maxWei: v.maxWei,
|
|
184
|
+
meanWei: v.count > 0 ? v.sumWei / BigInt(v.count) : 0n,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { perContract, globalMaxWei };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Detect anomalous spend in `candidate` receipts against a learned baseline.
|
|
191
|
+
* Flags: (a) a send to a contract id absent from the baseline, and (b) a value
|
|
192
|
+
* exceeding `maxMultiple ×` that contract's observed max. Deterministic; the
|
|
193
|
+
* baseline itself is never mutated. keyRef / private keys never enter here.
|
|
194
|
+
*/
|
|
195
|
+
export function detectAnomalies(candidate, baseline, opts = {}) {
|
|
196
|
+
const maxMultiple = BigInt(Math.max(1, Math.round(opts.maxMultiple ?? 2)));
|
|
197
|
+
const flagUnknown = opts.flagUnknownContracts !== false;
|
|
198
|
+
const anomalies = [];
|
|
199
|
+
for (const r of candidate) {
|
|
200
|
+
const base = baseline.perContract[r.contractId];
|
|
201
|
+
if (base === undefined) {
|
|
202
|
+
if (flagUnknown) {
|
|
203
|
+
anomalies.push({
|
|
204
|
+
ts: r.ts,
|
|
205
|
+
contractId: r.contractId,
|
|
206
|
+
valueWei: r.valueWei,
|
|
207
|
+
reason: `send to contract id "${r.contractId}" never seen in the learned baseline`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const ceiling = base.maxWei * maxMultiple;
|
|
213
|
+
if (r.valueWei > ceiling) {
|
|
214
|
+
anomalies.push({
|
|
215
|
+
ts: r.ts,
|
|
216
|
+
contractId: r.contractId,
|
|
217
|
+
valueWei: r.valueWei,
|
|
218
|
+
reason: `spend ${r.valueWei} wei exceeds ${maxMultiple}× the observed max ${base.maxWei} wei for "${r.contractId}"`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return anomalies;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Detect anomalous spend when candidate and baseline are the SAME history
|
|
226
|
+
* (`crewhaus onchain sentinel` invoked without `--baseline`) — the "self-
|
|
227
|
+
* compare" mode.
|
|
228
|
+
*
|
|
229
|
+
* A naive `learnSpendBaseline(history)` + `detectAnomalies(history, ...)`
|
|
230
|
+
* self-masks: a receipt's own value is folded into its contract's baseline
|
|
231
|
+
* max, so a lone 100× spike can never exceed `maxMultiple × (its own value)`.
|
|
232
|
+
* The value-ceiling check — the headline sentinel feature — is silently
|
|
233
|
+
* defeated in exactly the default (no-`--baseline`) invocation.
|
|
234
|
+
*
|
|
235
|
+
* Fix: leave-one-out. For each candidate receipt, the ceiling is computed
|
|
236
|
+
* from the max successful spend over every OTHER successful receipt for that
|
|
237
|
+
* contract — the receipt under test never contributes to its own ceiling. A
|
|
238
|
+
* lone spike is then measured against the normal population and correctly
|
|
239
|
+
* flagged; a normal history (every value within range of its peers) still
|
|
240
|
+
* flags nothing. Unknown-contract detection needs no leave-one-out (a
|
|
241
|
+
* contract either has ≥1 OTHER successful receipt or it doesn't — a receipt
|
|
242
|
+
* is never "unknown relative to itself" in a way that matters here, since a
|
|
243
|
+
* lone successful send to a contract is, by definition, its own baseline of
|
|
244
|
+
* one and cannot be a value anomaly against itself); this path only ever
|
|
245
|
+
* calls detectAnomalies-style value/unknown logic per receipt with a leave-
|
|
246
|
+
* one-out baseline substituted in.
|
|
247
|
+
*/
|
|
248
|
+
export function detectAnomaliesSelfCompare(history, opts = {}) {
|
|
249
|
+
const maxMultiple = BigInt(Math.max(1, Math.round(opts.maxMultiple ?? 2)));
|
|
250
|
+
const flagUnknown = opts.flagUnknownContracts !== false;
|
|
251
|
+
const ok = successfulReceipts(history);
|
|
252
|
+
// Precompute, per contract, the sorted successful wei values so a
|
|
253
|
+
// leave-one-out max can be derived in O(log n) per receipt instead of an
|
|
254
|
+
// O(n) rescan. (n is a receipt-history JSONL — expected small, but this
|
|
255
|
+
// keeps the self-compare path cheap regardless.)
|
|
256
|
+
const byContract = new Map();
|
|
257
|
+
for (const r of ok) {
|
|
258
|
+
const arr = byContract.get(r.contractId);
|
|
259
|
+
if (arr === undefined)
|
|
260
|
+
byContract.set(r.contractId, [r.valueWei]);
|
|
261
|
+
else
|
|
262
|
+
arr.push(r.valueWei);
|
|
263
|
+
}
|
|
264
|
+
const anomalies = [];
|
|
265
|
+
for (const r of history) {
|
|
266
|
+
const contractValues = byContract.get(r.contractId);
|
|
267
|
+
if (contractValues === undefined) {
|
|
268
|
+
// Only reachable for a non-successful (reverted/pending) receipt whose
|
|
269
|
+
// contract never had a successful send — genuinely unknown.
|
|
270
|
+
if (flagUnknown) {
|
|
271
|
+
anomalies.push({
|
|
272
|
+
ts: r.ts,
|
|
273
|
+
contractId: r.contractId,
|
|
274
|
+
valueWei: r.valueWei,
|
|
275
|
+
reason: `send to contract id "${r.contractId}" never seen in the learned baseline`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (r.status !== "0x1") {
|
|
281
|
+
// Reverted/pending receipts aren't part of the "legitimate spend"
|
|
282
|
+
// baseline and aren't value-checked here — mirrors the two-history
|
|
283
|
+
// path, where only successfulReceipts() feed learnSpendBaseline.
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
// Leave-one-out: the ceiling for THIS receipt excludes its own value —
|
|
287
|
+
// one occurrence of it is removed, then the max of the rest is taken.
|
|
288
|
+
let excludedOne = false;
|
|
289
|
+
let looMax = 0n;
|
|
290
|
+
for (const v of contractValues) {
|
|
291
|
+
if (!excludedOne && v === r.valueWei) {
|
|
292
|
+
excludedOne = true;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (v > looMax)
|
|
296
|
+
looMax = v;
|
|
297
|
+
}
|
|
298
|
+
if (!excludedOne) {
|
|
299
|
+
// Defensive: r.valueWei should always be in contractValues (it was
|
|
300
|
+
// pushed there while building the map). If not, fall back to the full
|
|
301
|
+
// max rather than under-count.
|
|
302
|
+
for (const v of contractValues)
|
|
303
|
+
if (v > looMax)
|
|
304
|
+
looMax = v;
|
|
305
|
+
}
|
|
306
|
+
// A contract with only this one successful receipt has no "other"
|
|
307
|
+
// population to compare against — nothing to flag as a ceiling breach.
|
|
308
|
+
if (contractValues.length < 2)
|
|
309
|
+
continue;
|
|
310
|
+
const ceiling = looMax * maxMultiple;
|
|
311
|
+
if (r.valueWei > ceiling) {
|
|
312
|
+
anomalies.push({
|
|
313
|
+
ts: r.ts,
|
|
314
|
+
contractId: r.contractId,
|
|
315
|
+
valueWei: r.valueWei,
|
|
316
|
+
reason: `spend ${r.valueWei} wei exceeds ${maxMultiple}× the observed max ${looMax} wei for "${r.contractId}" (leave-one-out)`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return anomalies;
|
|
321
|
+
}
|
|
322
|
+
/** Render a plain-text tune report. */
|
|
323
|
+
export function renderTuneReport(proposal) {
|
|
324
|
+
const lines = [];
|
|
325
|
+
lines.push("onchain policy tuner:");
|
|
326
|
+
lines.push(` proposed maxValueWei: ${proposal.proposedMaxValueWei ?? "(none — no successful spend)"}`);
|
|
327
|
+
lines.push(` proposed allowedContracts: [${proposal.proposedAllowedContracts.join(", ")}]`);
|
|
328
|
+
lines.push(` ${proposal.rationale}`);
|
|
329
|
+
for (const w of proposal.warnings)
|
|
330
|
+
lines.push(` ! ${w}`);
|
|
331
|
+
lines.push(proposal.patch !== undefined
|
|
332
|
+
? " → validated transaction_policy patch emitted (apply with optimize --write-back)"
|
|
333
|
+
: " → advice-only (transaction_policy is not optimizer-whitelisted for this target)");
|
|
334
|
+
return `${lines.join("\n")}\n`;
|
|
335
|
+
}
|
|
336
|
+
/** Render a plain-text sentinel report. */
|
|
337
|
+
export function renderSentinelReport(anomalies) {
|
|
338
|
+
if (anomalies.length === 0) {
|
|
339
|
+
return "onchain spend sentinel: no anomalies vs the learned baseline.\n";
|
|
340
|
+
}
|
|
341
|
+
const lines = [`onchain spend sentinel: ${anomalies.length} anomaly(ies)`];
|
|
342
|
+
for (const a of anomalies) {
|
|
343
|
+
lines.push(` ANOMALY ${a.contractId} @ts=${a.ts}: ${a.reason}`);
|
|
344
|
+
}
|
|
345
|
+
return `${lines.join("\n")}\n`;
|
|
346
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { PermissionRule, RuleType } from "@crewhaus/permission-engine";
|
|
2
|
+
import type { RegisteredTool } from "@crewhaus/tool-catalog";
|
|
3
|
+
import { type SessionEvents } from "./advise-rules";
|
|
4
|
+
export type AskAggregate = {
|
|
5
|
+
readonly toolName: string;
|
|
6
|
+
asks: number;
|
|
7
|
+
approved: number;
|
|
8
|
+
denied: number;
|
|
9
|
+
/** Sampled operative-arg values from adjacent tool_use inputs (deduped,
|
|
10
|
+
* capped). Empty ⇒ no arg pattern derivable → propose a bare tool glob. */
|
|
11
|
+
readonly argSamples: string[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The operative input field(s) per built-in tool — the one a permission
|
|
15
|
+
* arg-glob constrains. Re-exported from `@crewhaus/tool-permission-matcher`
|
|
16
|
+
* (the SINGLE source of truth) so a suggested pattern always targets the SAME
|
|
17
|
+
* field the matcher checks. This was hand-copied here (a silent-desync risk);
|
|
18
|
+
* now a matcher edit propagates automatically. Keyed by the PascalCase runtime
|
|
19
|
+
* `.name`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const OPERATIVE_ARG_FIELDS: Readonly<Record<string, readonly string[]>>;
|
|
22
|
+
/** Max distinct operative-arg samples kept per tool (bounds report size). */
|
|
23
|
+
export declare const MAX_ARG_SAMPLES = 5;
|
|
24
|
+
/**
|
|
25
|
+
* Fold sessions into per-tool ask aggregates. Reads `permission` lines for
|
|
26
|
+
* the ask counts and `tool_use` lines for the operative-arg samples. Pure;
|
|
27
|
+
* tolerant of old-vintage logs (no permission lines → empty result).
|
|
28
|
+
*/
|
|
29
|
+
export declare function aggregateAsks(sessions: ReadonlyArray<SessionEvents>): ReadonlyMap<string, AskAggregate>;
|
|
30
|
+
export type PermissionSuggestion = {
|
|
31
|
+
readonly rule: PermissionRule;
|
|
32
|
+
/** Why: "recurring-approved" | "recurring-denied". */
|
|
33
|
+
readonly reason: "recurring-approved" | "recurring-denied";
|
|
34
|
+
readonly toolName: string;
|
|
35
|
+
readonly readOnly: boolean;
|
|
36
|
+
readonly evidence: ReadonlyArray<string>;
|
|
37
|
+
/** The count the ranking sorts on (approvals or denials). */
|
|
38
|
+
readonly weight: number;
|
|
39
|
+
};
|
|
40
|
+
export type SuggestThresholds = {
|
|
41
|
+
/** Minimum ask prompts for a tool before any rule is proposed. */
|
|
42
|
+
readonly minAsks: number;
|
|
43
|
+
/** Fraction of asks approved at/above which an alwaysAllow is proposed. */
|
|
44
|
+
readonly approveRate: number;
|
|
45
|
+
/** Fraction of asks denied at/above which an alwaysAsk tightening fires. */
|
|
46
|
+
readonly denyRate: number;
|
|
47
|
+
};
|
|
48
|
+
export declare const DEFAULT_SUGGEST_THRESHOLDS: SuggestThresholds;
|
|
49
|
+
/** Build a name → readOnly lookup from the resolvable tool map (keyed by the
|
|
50
|
+
* RegisteredTool `.name`, which is what the ask aggregate is keyed by). */
|
|
51
|
+
export declare function readOnlyByName(toolMap: Readonly<Record<string, RegisteredTool>>): ReadonlyMap<string, boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* Compile a permission pattern for a tool. When exactly one operative-arg
|
|
54
|
+
* value recurred we emit a tool+arg glob (`Bash(git status)`); a small set of
|
|
55
|
+
* distinct values stays a bare tool glob (a per-value rule set would be noise
|
|
56
|
+
* — the human can tighten). Tools with no operative field (or no samples) get
|
|
57
|
+
* the bare tool name.
|
|
58
|
+
*
|
|
59
|
+
* SAFETY (glob widening): the observed value is a LITERAL the human approved,
|
|
60
|
+
* not a glob the human authored. tool-permission-matcher treats `*`/`?` in an
|
|
61
|
+
* arg-glob as wildcards, so splicing a raw value that contains them would make
|
|
62
|
+
* the suggested rule match unapproved siblings (`Bash(npm run test:*)` matching
|
|
63
|
+
* `Bash(npm run test:PRODUCTION-DELETE)`). We `escapeGlobLiteral` the value so
|
|
64
|
+
* the rule matches ONLY the approved string. A value containing `(`/`)` cannot
|
|
65
|
+
* be represented in the matcher's `Tool(arg)` format (its paren split isn't
|
|
66
|
+
* escape-aware), so we fall back to a bare tool glob rather than emit a rule
|
|
67
|
+
* that misparses — the reviewer can hand-write a tighter one.
|
|
68
|
+
*/
|
|
69
|
+
export declare function patternFor(agg: AskAggregate): string;
|
|
70
|
+
/**
|
|
71
|
+
* Rank ask aggregates into reviewable permission suggestions. Deterministic:
|
|
72
|
+
* - recurring-APPROVED tools (100% approved, ≥ minAsks) → `alwaysAllow`,
|
|
73
|
+
* read-only tools FIRST (lowest blast radius), then by approval count;
|
|
74
|
+
* - recurring-DENIED tools (deny rate ≥ denyRate) → `alwaysAsk` tightening
|
|
75
|
+
* (NOT alwaysDeny — a human denied a specific call, not the whole tool),
|
|
76
|
+
* by denial count.
|
|
77
|
+
* Read-only-ness comes from the tool map; an unknown tool defaults to false
|
|
78
|
+
* (treated as non-read-only ⇒ ranked after read-only grants).
|
|
79
|
+
*/
|
|
80
|
+
export declare function rankSuggestions(aggregates: ReadonlyMap<string, AskAggregate>, readOnly: ReadonlyMap<string, boolean>, thresholds?: SuggestThresholds): PermissionSuggestion[];
|
|
81
|
+
export type SettingsPermissionRule = {
|
|
82
|
+
readonly type: RuleType;
|
|
83
|
+
readonly pattern: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Read the existing `permissions.rules` out of a parsed `.crewhaus/settings.json`
|
|
87
|
+
* root (the EXACT shape `buildRuleSet` in index.ts consumes). Tolerant of a
|
|
88
|
+
* missing/foreign file: anything that isn't the expected shape yields `[]`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function existingSettingsRules(settingsRoot: unknown): SettingsPermissionRule[];
|
|
91
|
+
export type PermissionsDiff = {
|
|
92
|
+
/** The suggestions not already covered by an identical existing rule. */
|
|
93
|
+
readonly additions: ReadonlyArray<SettingsPermissionRule>;
|
|
94
|
+
/** Suggestions dropped because an identical rule already exists. */
|
|
95
|
+
readonly alreadyPresent: ReadonlyArray<SettingsPermissionRule>;
|
|
96
|
+
/** The full rule list after applying additions (existing ++ additions). */
|
|
97
|
+
readonly merged: ReadonlyArray<SettingsPermissionRule>;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Compute the additive diff between the existing settings rules and the
|
|
101
|
+
* ranked suggestions. A suggestion whose (type, pattern) already exists is
|
|
102
|
+
* reported as already-present, never duplicated. Order-preserving: existing
|
|
103
|
+
* rules first, then new additions in suggestion rank order.
|
|
104
|
+
*/
|
|
105
|
+
export declare function diffPermissions(existing: ReadonlyArray<SettingsPermissionRule>, suggestions: ReadonlyArray<PermissionSuggestion>): PermissionsDiff;
|
|
106
|
+
/**
|
|
107
|
+
* Render the settings.json `permissions` block that `--apply` would write,
|
|
108
|
+
* MERGING into the existing settings root so unrelated top-level keys
|
|
109
|
+
* (`hooks`, skills, …) survive. Returns the full new root object; the CLI
|
|
110
|
+
* JSON-stringifies it. Pure — never touches disk.
|
|
111
|
+
*/
|
|
112
|
+
export declare function applyToSettingsRoot(settingsRoot: unknown, merged: ReadonlyArray<SettingsPermissionRule>): Record<string, unknown>;
|
|
113
|
+
/** Human-readable suggestion lines for the CLI text mode. */
|
|
114
|
+
export declare function formatSuggestionLines(suggestions: ReadonlyArray<PermissionSuggestion>): string[];
|
|
115
|
+
/**
|
|
116
|
+
* A unified-diff-ish view of the settings.json permissions change: existing
|
|
117
|
+
* rules as context, additions as `+` lines. Purely for human review — this
|
|
118
|
+
* is NOT applied unless the user confirms `--apply` interactively.
|
|
119
|
+
*/
|
|
120
|
+
export declare function formatSettingsDiff(diff: PermissionsDiff): string[];
|
|
121
|
+
/**
|
|
122
|
+
* True when a pattern contains a glob wildcard (`*`/`?`) that is NOT
|
|
123
|
+
* backslash-escaped — i.e. one the matcher will treat as widening. A `\\*`/`\\?`
|
|
124
|
+
* (a literal escaped by `escapeGlobLiteral`) does not count.
|
|
125
|
+
*/
|
|
126
|
+
export declare function hasUnescapedWildcard(pattern: string): boolean;
|