@crewhaus/ir 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +154 -6
- package/dist/index.js +3 -1
- package/dist/readme.d.ts +74 -0
- package/dist/readme.js +381 -0
- package/dist/redact.d.ts +42 -0
- package/dist/redact.js +102 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -69,11 +69,14 @@ export type IrToolConfigs = Readonly<Record<string, unknown>>;
|
|
|
69
69
|
*/
|
|
70
70
|
export type IrCompaction = {
|
|
71
71
|
readonly model?: string;
|
|
72
|
-
/** Pillar 2 —
|
|
73
|
-
* as a pre-pass before the
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
72
|
+
/** Pillar 2 — RESERVED, not yet wired at runtime. Intended to make
|
|
73
|
+
* target emitters wire `compaction-curator` as a pre-pass before the
|
|
74
|
+
* autocompact threshold check, but no emitter or runtime-core path
|
|
75
|
+
* consumes this field today — setting it is currently a no-op. The
|
|
76
|
+
* spec layer accepts this verbatim (validated in `packages/spec`) and
|
|
77
|
+
* it lowers here unchanged so the value round-trips once wiring lands;
|
|
78
|
+
* the IR holds it as an opt-in flag with no default so emitters can
|
|
79
|
+
* eventually distinguish "user said false" from "user didn't say". */
|
|
77
80
|
readonly curate?: boolean;
|
|
78
81
|
/** Cosine threshold for the curator's dedupe pass. Curator's own
|
|
79
82
|
* default (0.92, `DEFAULT_DEDUPE_THRESHOLD` in
|
|
@@ -83,6 +86,38 @@ export type IrCompaction = {
|
|
|
83
86
|
* reorder without trimming. */
|
|
84
87
|
readonly relevanceTopK?: number;
|
|
85
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Item 22 — per-candidate circuit-breaker tuning lowered from the spec's
|
|
91
|
+
* `agent.circuit_breaker` block. Field names mirror `CircuitBreakerOptions`
|
|
92
|
+
* in `@crewhaus/circuit-breaker` exactly; every field optional so the
|
|
93
|
+
* breaker package's own defaults apply per knob. Carried on the agent
|
|
94
|
+
* blocks of the shapes wired for the failover chain (cli, channel,
|
|
95
|
+
* managed). Absent when the spec omits the block — declaring it WITHOUT
|
|
96
|
+
* `modelFallbacks` still breaker-wraps the single primary adapter.
|
|
97
|
+
*/
|
|
98
|
+
export type IrCircuitBreaker = {
|
|
99
|
+
readonly failureThreshold?: number;
|
|
100
|
+
readonly windowMs?: number;
|
|
101
|
+
readonly cooldownMs?: number;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Item 26 — two-tier turn-difficulty router config. `fast`/`default` are full
|
|
105
|
+
* model-router grammar strings; `routing` tunes the per-turn escalation
|
|
106
|
+
* thresholds (all optional — runtime defaults apply per knob). Carried on the
|
|
107
|
+
* agent blocks of the failover-capable shapes (cli, channel, managed). Absent
|
|
108
|
+
* when the spec omits `model_tiers` — codegen gates on presence so an unset
|
|
109
|
+
* block leaves bundles byte-identical.
|
|
110
|
+
*/
|
|
111
|
+
export type IrModelTiers = {
|
|
112
|
+
readonly fast: string;
|
|
113
|
+
readonly default: string;
|
|
114
|
+
readonly routing?: {
|
|
115
|
+
readonly contextTokenThreshold?: number;
|
|
116
|
+
readonly toolsToDefault?: boolean;
|
|
117
|
+
readonly firstTurnToDefault?: boolean;
|
|
118
|
+
readonly priorToolDensityThreshold?: number;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
86
121
|
/**
|
|
87
122
|
* Section 55 (Track A) — named failure taxonomy. Cross-cutting; carried
|
|
88
123
|
* through to runtime-core so `recovery-engine` can consult the user's
|
|
@@ -100,10 +135,30 @@ export type IrCompaction = {
|
|
|
100
135
|
export type IrFailureTaxonomyEntry = {
|
|
101
136
|
readonly class: string;
|
|
102
137
|
readonly pattern: string;
|
|
103
|
-
|
|
138
|
+
/** Item 23 — `switch-model` reroutes onto the next provider failover
|
|
139
|
+
* candidate mid-turn (see recovery-engine). */
|
|
140
|
+
readonly recovery: "retry" | "compact" | "continue" | "tombstone" | "switch-model" | "fail";
|
|
104
141
|
readonly hint?: string;
|
|
105
142
|
};
|
|
106
143
|
export type IrFailureTaxonomy = readonly IrFailureTaxonomyEntry[];
|
|
144
|
+
/**
|
|
145
|
+
* Item 27 — run-level spend cap with a degradation ladder, lowered from the
|
|
146
|
+
* spec's `budget` block. `usdMicros` is the dollar ceiling in USD-micros
|
|
147
|
+
* (1 USD = 1_000_000) — the unit the runtime meters in. `onExceed` decides
|
|
148
|
+
* the behaviour when accrued spend reaches the cap: `stop` ends the run
|
|
149
|
+
* before the next turn; `degrade` re-resolves the primary model to `model`
|
|
150
|
+
* (one cheaper rung) and continues. Carried on the interactive shapes that
|
|
151
|
+
* loop (cli, channel, managed); absent when the spec omits the block.
|
|
152
|
+
*/
|
|
153
|
+
export type IrBudget = {
|
|
154
|
+
readonly usdMicros: number;
|
|
155
|
+
readonly onExceed: {
|
|
156
|
+
readonly kind: "stop";
|
|
157
|
+
} | {
|
|
158
|
+
readonly kind: "degrade";
|
|
159
|
+
readonly model: string;
|
|
160
|
+
};
|
|
161
|
+
};
|
|
107
162
|
/**
|
|
108
163
|
* Pillar 3 (FR-004) — per-target security fabric configuration the
|
|
109
164
|
* compiler lowers from the spec's `security` block. Today it carries the
|
|
@@ -160,8 +215,56 @@ export type IrFeedback = {
|
|
|
160
215
|
readonly location: string;
|
|
161
216
|
};
|
|
162
217
|
readonly autoDistill?: boolean;
|
|
218
|
+
/** Item 1 — gate for the CLI REPL's one-keystroke exit rating prompt.
|
|
219
|
+
* Absent → prompt (the block's presence opts in); `false` → never. */
|
|
220
|
+
readonly exitPrompt?: boolean;
|
|
163
221
|
readonly channelReactions?: boolean;
|
|
164
222
|
};
|
|
223
|
+
/**
|
|
224
|
+
* Feature #53 — cross-session memory config, lowered from `spec.memory`.
|
|
225
|
+
* Presence of the block wires Remember/Recall into the target; the auto-*
|
|
226
|
+
* switches gate auto-capture (summarize durable outcomes at teardown) and
|
|
227
|
+
* auto-recall (inject top-K memories into the system prompt at session start).
|
|
228
|
+
* Carried on the interactive shapes that run a chat loop (IrV0/cli,
|
|
229
|
+
* IrChannelV0, IrManagedV0, IrResearchV0). Absent when the spec omits `memory`.
|
|
230
|
+
*/
|
|
231
|
+
export type IrMemory = {
|
|
232
|
+
readonly enabled?: boolean;
|
|
233
|
+
readonly autoCapture?: boolean;
|
|
234
|
+
readonly autoCaptureThreshold?: number;
|
|
235
|
+
readonly autoRecall?: boolean;
|
|
236
|
+
readonly recallK?: number;
|
|
237
|
+
};
|
|
238
|
+
/** Ops item 37 — a mitigation-ladder rung the runtime SLO monitor walks on a
|
|
239
|
+
* sustained breach, in declared order. See {@link IrSlo}. */
|
|
240
|
+
export type IrSloMitigation = "alert" | "pause-intake" | "rollback";
|
|
241
|
+
/**
|
|
242
|
+
* Ops item 37 — production SLO targets + the mitigation ladder, lowered from
|
|
243
|
+
* `spec.observability.slo`. Every target is optional (declare only the ones you
|
|
244
|
+
* care about); an omitted target is never evaluated by the monitor. `windowMs`
|
|
245
|
+
* is the rolling window a breach must persist before the ladder fires (spec's
|
|
246
|
+
* `window_seconds` × 1000; the monitor defaults it when absent). `mitigation`
|
|
247
|
+
* defaults to `["alert"]` at lower time so an observe-only spec still warns.
|
|
248
|
+
* Absent from the IR when the spec omits the block.
|
|
249
|
+
*/
|
|
250
|
+
export type IrSlo = {
|
|
251
|
+
readonly errorRate?: number;
|
|
252
|
+
readonly p95LatencyMs?: number;
|
|
253
|
+
readonly ttftMs?: number;
|
|
254
|
+
readonly costPerHourUsd?: number;
|
|
255
|
+
readonly egressBlockRate?: number;
|
|
256
|
+
readonly windowMs?: number;
|
|
257
|
+
readonly mitigation: ReadonlyArray<IrSloMitigation>;
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Ops item 37 — cross-cutting observability config, lowered from
|
|
261
|
+
* `spec.observability`. Today it carries one sub-block, `slo`. Carried on the
|
|
262
|
+
* interactive/daemon shapes that run a chat loop (IrV0/cli, IrChannelV0,
|
|
263
|
+
* IrManagedV0). Absent when the spec omits the `observability` block.
|
|
264
|
+
*/
|
|
265
|
+
export type IrObservability = {
|
|
266
|
+
readonly slo?: IrSlo;
|
|
267
|
+
};
|
|
165
268
|
/**
|
|
166
269
|
* Track F (Section 57) — typed message schemas (Σ) for multi-agent
|
|
167
270
|
* communication. Source: AgentFlow (arxiv 2604.20801). A typed graph
|
|
@@ -216,6 +319,14 @@ export type IrV0 = {
|
|
|
216
319
|
/** Model max OUTPUT tokens for one turn (spec `agent.max_tokens`).
|
|
217
320
|
* Optional; when absent the runtime default applies. */
|
|
218
321
|
readonly maxTokens?: number;
|
|
322
|
+
/** Item 22 — ordered failover models (spec `agent.model_fallbacks`).
|
|
323
|
+
* Absent when the spec omits the block; the runtime then keeps its
|
|
324
|
+
* single-adapter path. */
|
|
325
|
+
readonly modelFallbacks?: readonly string[];
|
|
326
|
+
/** Item 22 — breaker tuning (spec `agent.circuit_breaker`). */
|
|
327
|
+
readonly circuitBreaker?: IrCircuitBreaker;
|
|
328
|
+
/** Item 26 — two-tier turn-difficulty router. Absent → single-model. */
|
|
329
|
+
readonly modelTiers?: IrModelTiers;
|
|
219
330
|
};
|
|
220
331
|
readonly tools: readonly string[];
|
|
221
332
|
readonly toolConfigs: IrToolConfigs;
|
|
@@ -226,12 +337,19 @@ export type IrV0 = {
|
|
|
226
337
|
readonly cli?: IrCliOptions;
|
|
227
338
|
/** Section 55 (Track A) — named failure taxonomy. Optional. */
|
|
228
339
|
readonly failureTaxonomy?: IrFailureTaxonomy;
|
|
340
|
+
/** Item 27 — run-level spend cap + degradation ladder. Optional. */
|
|
341
|
+
readonly budget?: IrBudget;
|
|
229
342
|
/** Pillar 3 (FR-004) — security fabric config (intent-gate judge
|
|
230
343
|
* selection). Optional; absent when the spec omits the `security`
|
|
231
344
|
* block. */
|
|
232
345
|
readonly security?: IrSecurity;
|
|
233
346
|
/** Response-feedback config. Optional; absent when the spec omits `feedback`. */
|
|
234
347
|
readonly feedback?: IrFeedback;
|
|
348
|
+
/** #53 cross-session memory config. Optional; absent when the spec omits `memory`. */
|
|
349
|
+
readonly memory?: IrMemory;
|
|
350
|
+
/** Ops item 37 — SLO targets + mitigation ladder. Optional; absent when the
|
|
351
|
+
* spec omits the `observability` block. */
|
|
352
|
+
readonly observability?: IrObservability;
|
|
235
353
|
/** §47 cross-cutting blockchain subsystem (slice 0). All optional. */
|
|
236
354
|
readonly chains?: readonly IrChainBinding[];
|
|
237
355
|
readonly wallets?: readonly IrWalletBinding[];
|
|
@@ -463,6 +581,12 @@ export type IrChannelV0 = {
|
|
|
463
581
|
readonly agent: {
|
|
464
582
|
readonly model: string;
|
|
465
583
|
readonly instructions: string;
|
|
584
|
+
/** Item 22 — ordered failover models (spec `agent.model_fallbacks`). */
|
|
585
|
+
readonly modelFallbacks?: readonly string[];
|
|
586
|
+
/** Item 22 — breaker tuning (spec `agent.circuit_breaker`). */
|
|
587
|
+
readonly circuitBreaker?: IrCircuitBreaker;
|
|
588
|
+
/** Item 26 — two-tier turn-difficulty router. Absent → single-model. */
|
|
589
|
+
readonly modelTiers?: IrModelTiers;
|
|
466
590
|
};
|
|
467
591
|
readonly tools: readonly string[];
|
|
468
592
|
readonly toolConfigs: IrToolConfigs;
|
|
@@ -476,9 +600,16 @@ export type IrChannelV0 = {
|
|
|
476
600
|
readonly gateway?: IrChannelGateway;
|
|
477
601
|
/** Section 55 (Track A) — named failure taxonomy. Optional. */
|
|
478
602
|
readonly failureTaxonomy?: IrFailureTaxonomy;
|
|
603
|
+
/** Item 27 — run-level spend cap + degradation ladder. Optional. */
|
|
604
|
+
readonly budget?: IrBudget;
|
|
479
605
|
/** Response-feedback config. `feedback.channelReactions` gates Slack 👍/👎
|
|
480
606
|
* → user_feedback codegen in this target. Absent when spec omits it. */
|
|
481
607
|
readonly feedback?: IrFeedback;
|
|
608
|
+
/** #53 cross-session memory config. Optional; absent when the spec omits `memory`. */
|
|
609
|
+
readonly memory?: IrMemory;
|
|
610
|
+
/** Ops item 37 — SLO targets + mitigation ladder. Optional; absent when the
|
|
611
|
+
* spec omits the `observability` block. */
|
|
612
|
+
readonly observability?: IrObservability;
|
|
482
613
|
/** §47 cross-cutting blockchain subsystem (slice 0). All optional. */
|
|
483
614
|
readonly chains?: readonly IrChainBinding[];
|
|
484
615
|
readonly wallets?: readonly IrWalletBinding[];
|
|
@@ -505,12 +636,26 @@ export type IrManagedV0 = {
|
|
|
505
636
|
readonly agent: {
|
|
506
637
|
readonly model: string;
|
|
507
638
|
readonly instructions: string;
|
|
639
|
+
/** Item 22 — ordered failover models (spec `agent.model_fallbacks`). */
|
|
640
|
+
readonly modelFallbacks?: readonly string[];
|
|
641
|
+
/** Item 22 — breaker tuning (spec `agent.circuit_breaker`). */
|
|
642
|
+
readonly circuitBreaker?: IrCircuitBreaker;
|
|
643
|
+
/** Item 26 — two-tier turn-difficulty router. Absent → single-model. */
|
|
644
|
+
readonly modelTiers?: IrModelTiers;
|
|
508
645
|
};
|
|
509
646
|
readonly tenants: readonly IrManagedTenant[];
|
|
510
647
|
readonly permissions: IrPermissions;
|
|
511
648
|
readonly compaction: IrCompaction;
|
|
512
649
|
/** Section 55 (Track A) — named failure taxonomy. Optional. */
|
|
513
650
|
readonly failureTaxonomy?: IrFailureTaxonomy;
|
|
651
|
+
/** Item 27 — run-level spend cap + degradation ladder. Optional. */
|
|
652
|
+
readonly budget?: IrBudget;
|
|
653
|
+
/** #53 cross-session memory config. Optional; absent when the spec omits `memory`. */
|
|
654
|
+
readonly memory?: IrMemory;
|
|
655
|
+
/** Ops item 37 — SLO targets + mitigation ladder. Optional; absent when the
|
|
656
|
+
* spec omits the `observability` block. The managed daemon's `pause-intake`
|
|
657
|
+
* rung reuses its `budget_exceeded` 429 path. */
|
|
658
|
+
readonly observability?: IrObservability;
|
|
514
659
|
};
|
|
515
660
|
/**
|
|
516
661
|
* Section 19 — Graph IR. A `target: "graph"` spec lowers into a fixed
|
|
@@ -700,6 +845,8 @@ export type IrResearchV0 = {
|
|
|
700
845
|
readonly compaction: IrCompaction;
|
|
701
846
|
/** Section 55 (Track A) — named failure taxonomy. Optional. */
|
|
702
847
|
readonly failureTaxonomy?: IrFailureTaxonomy;
|
|
848
|
+
/** #53 cross-session memory config. Optional; absent when the spec omits `memory`. */
|
|
849
|
+
readonly memory?: IrMemory;
|
|
703
850
|
/** §47 cross-cutting blockchain subsystem (slice 0). All optional. */
|
|
704
851
|
readonly chains?: readonly IrChainBinding[];
|
|
705
852
|
readonly wallets?: readonly IrWalletBinding[];
|
|
@@ -969,3 +1116,4 @@ export type Bundle = {
|
|
|
969
1116
|
readonly content: string;
|
|
970
1117
|
}>;
|
|
971
1118
|
};
|
|
1119
|
+
export { type BundleReadmeOptions, type BundleReadmeSection, type CollectedSecretRefs, type EmitReadmeOptions, GENERATED_README_MARKER, collectSecretRefs, renderBundleReadme, } from "./readme";
|
package/dist/index.js
CHANGED
package/dist/readme.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated-bundle README renderer (AUTOMATION-OPPORTUNITIES.md item 42).
|
|
3
|
+
*
|
|
4
|
+
* Every target emitter drops a `README.md` into its compiled bundle so a
|
|
5
|
+
* user who cd's into an out-dir can see — without reading generated code —
|
|
6
|
+
* what the harness is, which tools/MCP servers it wires, WHICH ENV VARS it
|
|
7
|
+
* needs, and how to launch it. The renderer lives here in `@crewhaus/ir`
|
|
8
|
+
* because it is a pure function over the lowered IR and this is the one
|
|
9
|
+
* package every `target-*` emitter already depends on (adding it here
|
|
10
|
+
* creates zero new dependency edges; `packages/compiler` sits *downstream*
|
|
11
|
+
* of the emitters, so it cannot be the seam).
|
|
12
|
+
*
|
|
13
|
+
* Security invariant: secret refs lowered to `{ kind: "literal" }` are
|
|
14
|
+
* NEVER printed — the README only names `{ kind: "env" }` variables. A
|
|
15
|
+
* literal credential is already a spec smell (see `lowerCredential` in
|
|
16
|
+
* `packages/compiler`), and the README must not widen the blast radius by
|
|
17
|
+
* copying the value into a second, more-readable artifact.
|
|
18
|
+
*/
|
|
19
|
+
import type { IrNode } from "./index";
|
|
20
|
+
/**
|
|
21
|
+
* Common emitter option controlling README.md emission. Default ON;
|
|
22
|
+
* `crewhaus compile --no-readme` threads `readme: false` through
|
|
23
|
+
* `CompileOptions` to every target emitter.
|
|
24
|
+
*/
|
|
25
|
+
export type EmitReadmeOptions = {
|
|
26
|
+
readonly readme?: boolean;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Machine-checkable marker embedded in every generated README. The CLI's
|
|
30
|
+
* `compile` write path uses it to distinguish a previously-generated
|
|
31
|
+
* README (safe to overwrite on recompile) from a user-authored one
|
|
32
|
+
* (kept, with a notice).
|
|
33
|
+
*/
|
|
34
|
+
export declare const GENERATED_README_MARKER = "<!-- crewhaus:generated-readme -->";
|
|
35
|
+
/** A README section: markdown heading text + markdown body. */
|
|
36
|
+
export type BundleReadmeSection = {
|
|
37
|
+
readonly heading: string;
|
|
38
|
+
readonly body: string;
|
|
39
|
+
};
|
|
40
|
+
export type BundleReadmeOptions = {
|
|
41
|
+
/** One-line description under the title. Defaults to a generated line. */
|
|
42
|
+
readonly description?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Replace the default per-target Run section (e.g. the cf-worker
|
|
45
|
+
* emitters substitute a `wrangler deploy` flow, the claude-plugin
|
|
46
|
+
* emitter an Install note).
|
|
47
|
+
*/
|
|
48
|
+
readonly usage?: BundleReadmeSection;
|
|
49
|
+
/** Include the `.crewhaus/` runtime-data note. Default true. */
|
|
50
|
+
readonly includeWorkspaceNote?: boolean;
|
|
51
|
+
/** Extra sections appended at the end (e.g. claude-plugin's Origin). */
|
|
52
|
+
readonly extraSections?: readonly BundleReadmeSection[];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Every env var / redacted-literal count referenced by a lowered IR's
|
|
56
|
+
* secret-shaped fields (`IrSecretRef`), gathered via a recursive walk so
|
|
57
|
+
* variant-specific nesting (channel credentials, chain `rpcUrls`, wallet
|
|
58
|
+
* `keyRef`, pipeline `retrieve.apiKey`, …) is covered without coupling to
|
|
59
|
+
* each variant's shape — the same discipline as `collectToolNames` in
|
|
60
|
+
* `packages/compiler`.
|
|
61
|
+
*/
|
|
62
|
+
export type CollectedSecretRefs = {
|
|
63
|
+
/** Deduped, sorted `{ kind: "env" }` variable names. */
|
|
64
|
+
readonly envNames: readonly string[];
|
|
65
|
+
/** How many `{ kind: "literal" }` refs were found. Values are REDACTED. */
|
|
66
|
+
readonly literalCount: number;
|
|
67
|
+
};
|
|
68
|
+
export declare function collectSecretRefs(ir: unknown): CollectedSecretRefs;
|
|
69
|
+
/**
|
|
70
|
+
* Render the generated bundle README from a lowered IR. Pure; no I/O.
|
|
71
|
+
* Deterministic for a given IR (tables and env lists are sorted) so
|
|
72
|
+
* recompiles diff cleanly.
|
|
73
|
+
*/
|
|
74
|
+
export declare function renderBundleReadme(ir: IrNode, opts?: BundleReadmeOptions): string;
|
package/dist/readme.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { OPAQUE_TOKEN_RE, maskCredentialTokens } from "./redact";
|
|
2
|
+
/**
|
|
3
|
+
* Machine-checkable marker embedded in every generated README. The CLI's
|
|
4
|
+
* `compile` write path uses it to distinguish a previously-generated
|
|
5
|
+
* README (safe to overwrite on recompile) from a user-authored one
|
|
6
|
+
* (kept, with a notice).
|
|
7
|
+
*/
|
|
8
|
+
export const GENERATED_README_MARKER = "<!-- crewhaus:generated-readme -->";
|
|
9
|
+
export function collectSecretRefs(ir) {
|
|
10
|
+
const envNames = new Set();
|
|
11
|
+
let literalCount = 0;
|
|
12
|
+
const visit = (node) => {
|
|
13
|
+
if (Array.isArray(node)) {
|
|
14
|
+
for (const item of node)
|
|
15
|
+
visit(item);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (node === null || typeof node !== "object")
|
|
19
|
+
return;
|
|
20
|
+
const record = node;
|
|
21
|
+
// Match the IrSecretRef discriminants exactly. Other IR unions also
|
|
22
|
+
// carry a `kind` key (`IrChainFinality`, `IrSchemaRef`, triggers) but
|
|
23
|
+
// none uses the values "env"/"literal", so this cannot misfire.
|
|
24
|
+
if (record["kind"] === "env" && typeof record["name"] === "string") {
|
|
25
|
+
envNames.add(record["name"]);
|
|
26
|
+
}
|
|
27
|
+
else if (record["kind"] === "literal" && typeof record["value"] === "string") {
|
|
28
|
+
literalCount += 1;
|
|
29
|
+
}
|
|
30
|
+
for (const value of Object.values(record))
|
|
31
|
+
visit(value);
|
|
32
|
+
};
|
|
33
|
+
visit(ir);
|
|
34
|
+
return { envNames: [...envNames].sort(), literalCount };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Per-shape launch one-liner, derived from each emitter's output layout:
|
|
38
|
+
* shapes that emit a `daemon.ts` entrypoint (channel / managed / crew /
|
|
39
|
+
* voice) launch that; every other shape's entrypoint is `agent.ts`.
|
|
40
|
+
*/
|
|
41
|
+
const RUN_COMMANDS = {
|
|
42
|
+
cli: "bun agent.ts",
|
|
43
|
+
workflow: "bun agent.ts",
|
|
44
|
+
channel: "bun daemon.ts",
|
|
45
|
+
graph: "bun agent.ts",
|
|
46
|
+
managed: "bun daemon.ts",
|
|
47
|
+
pipeline: "bun agent.ts",
|
|
48
|
+
crew: "bun daemon.ts",
|
|
49
|
+
research: "bun agent.ts",
|
|
50
|
+
batch: "bun agent.ts",
|
|
51
|
+
voice: "bun daemon.ts",
|
|
52
|
+
browser: "bun agent.ts",
|
|
53
|
+
eval: "bun agent.ts",
|
|
54
|
+
onchain: "bun agent.ts",
|
|
55
|
+
"onchain-game": "bun agent.ts",
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Definitionally outward-reaching tool names, in both the spec-key
|
|
59
|
+
* (camelCase) and registered (PascalCase) forms the IR can carry. Mirrors
|
|
60
|
+
* `OUTWARD_TOOL_NAMES` in `@crewhaus/tool-builder` — the canonical rule —
|
|
61
|
+
* but kept inline (exactly as `IrVectorBackend` mirrors `vector-store`)
|
|
62
|
+
* so the runtime-agnostic IR keeps its zero package dependencies. Keep in
|
|
63
|
+
* sync when a name is added or removed.
|
|
64
|
+
*/
|
|
65
|
+
const OUTWARD_TOOL_NAMES = new Set([
|
|
66
|
+
"fetch",
|
|
67
|
+
"Fetch",
|
|
68
|
+
"webFetch",
|
|
69
|
+
"WebFetch",
|
|
70
|
+
"webSearch",
|
|
71
|
+
"WebSearch",
|
|
72
|
+
"sendMessage",
|
|
73
|
+
"SendMessage",
|
|
74
|
+
"evmSendTransaction",
|
|
75
|
+
"EvmSendTransaction",
|
|
76
|
+
"imageGenerate",
|
|
77
|
+
"ImageGenerate",
|
|
78
|
+
]);
|
|
79
|
+
/** Tools executed inside the §18 sandbox (see `target-cli`'s sandbox gate). */
|
|
80
|
+
const SANDBOXED_TOOL_NAMES = new Set(["python", "javascript", "shell"]);
|
|
81
|
+
/**
|
|
82
|
+
* Tools that `requireJustification: true` by default (the Pillar 3 intent
|
|
83
|
+
* gate — see AGENTS.md). Both name forms, same mirroring caveat as above.
|
|
84
|
+
*/
|
|
85
|
+
const JUSTIFICATION_GATED_TOOL_NAMES = new Set([
|
|
86
|
+
"sendMessage",
|
|
87
|
+
"SendMessage",
|
|
88
|
+
"evmSendTransaction",
|
|
89
|
+
"EvmSendTransaction",
|
|
90
|
+
"imageGenerate",
|
|
91
|
+
"ImageGenerate",
|
|
92
|
+
]);
|
|
93
|
+
/** Keys whose array items scope the `tools` lists nested beneath them. */
|
|
94
|
+
const NESTED_TOOL_CONTEXTS = {
|
|
95
|
+
steps: "step",
|
|
96
|
+
nodes: "node",
|
|
97
|
+
roles: "role",
|
|
98
|
+
subAgents: "sub-agent",
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Recursive walk gathering every string under a `tools` key together with
|
|
102
|
+
* the context that declares it ("agent" at the top level; `step \`x\`` /
|
|
103
|
+
* `node \`x\`` / `role \`x\`` / `sub-agent \`x\`` when nested). Mirrors
|
|
104
|
+
* `collectToolNames` in `packages/compiler`, with context tracking added.
|
|
105
|
+
*/
|
|
106
|
+
function collectToolUsage(ir) {
|
|
107
|
+
const usage = new Map();
|
|
108
|
+
const add = (tool, context) => {
|
|
109
|
+
const contexts = usage.get(tool) ?? new Set();
|
|
110
|
+
contexts.add(context);
|
|
111
|
+
usage.set(tool, contexts);
|
|
112
|
+
};
|
|
113
|
+
const visit = (node, context) => {
|
|
114
|
+
if (Array.isArray(node)) {
|
|
115
|
+
for (const item of node)
|
|
116
|
+
visit(item, context);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (node === null || typeof node !== "object")
|
|
120
|
+
return;
|
|
121
|
+
for (const [key, value] of Object.entries(node)) {
|
|
122
|
+
if (key === "tools" && Array.isArray(value)) {
|
|
123
|
+
for (const v of value)
|
|
124
|
+
if (typeof v === "string")
|
|
125
|
+
add(v, context);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const label = NESTED_TOOL_CONTEXTS[key];
|
|
129
|
+
if (label !== undefined && Array.isArray(value)) {
|
|
130
|
+
for (const item of value) {
|
|
131
|
+
const name = item?.name;
|
|
132
|
+
// The context label lands in a table cell verbatim — escape the
|
|
133
|
+
// interpolated name (F5) but not the intentional backticks here.
|
|
134
|
+
visit(item, typeof name === "string" ? `${label} \`${escapeCell(name)}\`` : label);
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
visit(value, context);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
visit(ir, "agent");
|
|
142
|
+
return usage;
|
|
143
|
+
}
|
|
144
|
+
/** Every tool name carrying a `tool_config` blob, across nested variants. */
|
|
145
|
+
function collectConfiguredToolNames(ir) {
|
|
146
|
+
const configured = new Set();
|
|
147
|
+
const visit = (node) => {
|
|
148
|
+
if (Array.isArray(node)) {
|
|
149
|
+
for (const item of node)
|
|
150
|
+
visit(item);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (node === null || typeof node !== "object")
|
|
154
|
+
return;
|
|
155
|
+
for (const [key, value] of Object.entries(node)) {
|
|
156
|
+
if (key === "toolConfigs" && value !== null && typeof value === "object") {
|
|
157
|
+
for (const name of Object.keys(value))
|
|
158
|
+
configured.add(name);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
visit(value);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
visit(ir);
|
|
165
|
+
return configured;
|
|
166
|
+
}
|
|
167
|
+
/** Deduped models across variant shapes (agent / steps / nodes / roles). */
|
|
168
|
+
function collectModels(ir) {
|
|
169
|
+
switch (ir.target) {
|
|
170
|
+
case "workflow":
|
|
171
|
+
return [...new Set(ir.steps.map((s) => s.model))];
|
|
172
|
+
case "graph":
|
|
173
|
+
return [...new Set(ir.nodes.map((n) => n.model))];
|
|
174
|
+
case "crew":
|
|
175
|
+
return [...new Set(ir.roles.map((r) => r.model))];
|
|
176
|
+
default:
|
|
177
|
+
return [ir.agent.model];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Adversarial-review F5 — escape an interpolated value for a GFM table
|
|
182
|
+
* cell: `|` would split the cell, a raw newline would end the row, and a
|
|
183
|
+
* stray backtick would terminate the code-span the tables wrap values in.
|
|
184
|
+
*/
|
|
185
|
+
function escapeCell(value) {
|
|
186
|
+
return value.replace(/\|/g, "\\|").replace(/`/g, "\\`").replace(/\r?\n/g, " ");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Adversarial-review F2 — words in a CLI flag name that mark its VALUE as a
|
|
190
|
+
* credential (`--token=…`, `--api-key …`). Matched per dash-separated word
|
|
191
|
+
* so `--keyboard-layout` stays visible while `--api-key` masks.
|
|
192
|
+
*/
|
|
193
|
+
const CREDENTIAL_FLAG_WORDS = new Set([
|
|
194
|
+
"key",
|
|
195
|
+
"apikey",
|
|
196
|
+
"token",
|
|
197
|
+
"secret",
|
|
198
|
+
"password",
|
|
199
|
+
"pass",
|
|
200
|
+
"passwd",
|
|
201
|
+
"pwd",
|
|
202
|
+
"auth",
|
|
203
|
+
"authorization",
|
|
204
|
+
"credential",
|
|
205
|
+
"credentials",
|
|
206
|
+
"bearer",
|
|
207
|
+
]);
|
|
208
|
+
function isCredentialFlag(arg) {
|
|
209
|
+
const m = arg.match(/^--?([A-Za-z][A-Za-z0-9-]*)$/);
|
|
210
|
+
if (m?.[1] === undefined)
|
|
211
|
+
return false;
|
|
212
|
+
return m[1]
|
|
213
|
+
.toLowerCase()
|
|
214
|
+
.split("-")
|
|
215
|
+
.some((word) => CREDENTIAL_FLAG_WORDS.has(word));
|
|
216
|
+
}
|
|
217
|
+
/** Render a stdio server's launch line with credential-valued flags masked
|
|
218
|
+
* (`--token=v` and `--token v` forms) and known token shapes (`sk-…`,
|
|
219
|
+
* `ghp_…`, …) masked out of every other arg. */
|
|
220
|
+
function maskStdioCommand(command, args) {
|
|
221
|
+
const masked = [];
|
|
222
|
+
let maskNext = false;
|
|
223
|
+
for (const arg of args) {
|
|
224
|
+
if (maskNext) {
|
|
225
|
+
masked.push("***");
|
|
226
|
+
maskNext = false;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const eq = arg.indexOf("=");
|
|
230
|
+
if (arg.startsWith("-") && eq > 0 && isCredentialFlag(arg.slice(0, eq))) {
|
|
231
|
+
masked.push(`${arg.slice(0, eq)}=***`);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (isCredentialFlag(arg)) {
|
|
235
|
+
masked.push(arg);
|
|
236
|
+
maskNext = true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
masked.push(maskCredentialTokens(arg));
|
|
240
|
+
}
|
|
241
|
+
return [command, ...masked].join(" ");
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Mask the credential-bearing parts of an sse URL: userinfo passwords
|
|
245
|
+
* (`https://user:***@`), ALL query-parameter values (`?apikey=***` — key
|
|
246
|
+
* names stay, values never render), and path segments shaped like opaque
|
|
247
|
+
* credentials (Alchemy/Infura-style `/v2/<key>` → `/v2/***`). Regex-based
|
|
248
|
+
* so a not-quite-parseable URL still gets masked rather than printed raw.
|
|
249
|
+
*/
|
|
250
|
+
function maskUrlCredentials(url) {
|
|
251
|
+
// Userinfo: keep the user, mask the password.
|
|
252
|
+
const out = url.replace(/^([a-z][a-z0-9+.-]*:\/\/[^/@:]+):([^/@]+)@/i, "$1:***@");
|
|
253
|
+
const q = out.indexOf("?");
|
|
254
|
+
const query = q === -1 ? "" : out.slice(q).replace(/([?&;][^=&;#]*)=([^&;#]*)/g, "$1=***");
|
|
255
|
+
let base = q === -1 ? out : out.slice(0, q);
|
|
256
|
+
const prefix = base.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/i)?.[0] ?? "";
|
|
257
|
+
const path = base
|
|
258
|
+
.slice(prefix.length)
|
|
259
|
+
.split("/")
|
|
260
|
+
.map((seg) => (OPAQUE_TOKEN_RE.test(seg) ? "***" : maskCredentialTokens(seg)))
|
|
261
|
+
.join("/");
|
|
262
|
+
base = `${prefix}${path}`;
|
|
263
|
+
return `${base}${query}`;
|
|
264
|
+
}
|
|
265
|
+
function toolScopeHint(name) {
|
|
266
|
+
if (name.startsWith("mcp__"))
|
|
267
|
+
return "external (MCP)";
|
|
268
|
+
if (OUTWARD_TOOL_NAMES.has(name))
|
|
269
|
+
return "external";
|
|
270
|
+
return "built-in";
|
|
271
|
+
}
|
|
272
|
+
function toolNotes(name, configured) {
|
|
273
|
+
const notes = [];
|
|
274
|
+
if (SANDBOXED_TOOL_NAMES.has(name))
|
|
275
|
+
notes.push("sandboxed");
|
|
276
|
+
if (JUSTIFICATION_GATED_TOOL_NAMES.has(name))
|
|
277
|
+
notes.push("justification-gated by default");
|
|
278
|
+
if (configured.has(name))
|
|
279
|
+
notes.push("configured via `tool_config`");
|
|
280
|
+
return notes.length > 0 ? notes.join("; ") : "—";
|
|
281
|
+
}
|
|
282
|
+
function renderToolsSection(ir) {
|
|
283
|
+
const usage = collectToolUsage(ir);
|
|
284
|
+
if (usage.size === 0)
|
|
285
|
+
return undefined;
|
|
286
|
+
const configured = collectConfiguredToolNames(ir);
|
|
287
|
+
const rows = [...usage.keys()].sort().map((name) => {
|
|
288
|
+
const contexts = [...(usage.get(name) ?? new Set())].sort().join(", ");
|
|
289
|
+
return `| \`${escapeCell(name)}\` | ${contexts} | ${toolScopeHint(name)} | ${toolNotes(name, configured)} |`;
|
|
290
|
+
});
|
|
291
|
+
return ["| Tool | Used by | Scope | Notes |", "| --- | --- | --- | --- |", ...rows].join("\n");
|
|
292
|
+
}
|
|
293
|
+
function renderMcpSection(ir) {
|
|
294
|
+
const servers = ir.mcp_servers;
|
|
295
|
+
if (servers === undefined)
|
|
296
|
+
return undefined;
|
|
297
|
+
const entries = Object.entries(servers);
|
|
298
|
+
if (entries.length === 0)
|
|
299
|
+
return undefined;
|
|
300
|
+
// Endpoint column: command for stdio, URL for sse. Env values and sse
|
|
301
|
+
// headers are intentionally NOT rendered — they can carry credentials —
|
|
302
|
+
// and the command/URL themselves are masked (adversarial-review F2):
|
|
303
|
+
// credential-valued flags, known token shapes in args, URL userinfo
|
|
304
|
+
// passwords, ALL query-parameter values, and opaque path segments.
|
|
305
|
+
const rows = entries.map(([name, cfg]) => cfg.transport === "stdio"
|
|
306
|
+
? `| \`${escapeCell(name)}\` | stdio | \`${escapeCell(maskStdioCommand(cfg.command, cfg.args))}\` |`
|
|
307
|
+
: `| \`${escapeCell(name)}\` | sse | \`${escapeCell(maskUrlCredentials(cfg.url))}\` |`);
|
|
308
|
+
return ["| Server | Transport | Endpoint |", "| --- | --- | --- |", ...rows].join("\n");
|
|
309
|
+
}
|
|
310
|
+
function renderEnvSection(ir) {
|
|
311
|
+
const { envNames, literalCount } = collectSecretRefs(ir);
|
|
312
|
+
const models = collectModels(ir);
|
|
313
|
+
const lines = [];
|
|
314
|
+
if (envNames.length > 0) {
|
|
315
|
+
lines.push("Set these before launching — the bundle reads them from `process.env` at runtime:", "", ...envNames.map((name) => `- \`${name}\``));
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
lines.push("No spec-declared environment variables.");
|
|
319
|
+
}
|
|
320
|
+
if (literalCount > 0) {
|
|
321
|
+
lines.push("", `> ${literalCount} secret-shaped value(s) were supplied as literals in the spec and are compiled into the bundle — they are not shown here. Prefer \`$UPPER_SNAKE_CASE\` env references so credentials stay out of compiled artifacts.`);
|
|
322
|
+
}
|
|
323
|
+
lines.push("", `The model provider's API key must also be present (e.g. \`ANTHROPIC_API_KEY\` for Anthropic \`claude-*\` models; model(s) in use: ${models
|
|
324
|
+
.map((m) => `\`${m}\``)
|
|
325
|
+
.join(", ")}).`);
|
|
326
|
+
return lines.join("\n");
|
|
327
|
+
}
|
|
328
|
+
function defaultUsageSection(ir) {
|
|
329
|
+
return {
|
|
330
|
+
heading: "Run",
|
|
331
|
+
body: ["```sh", RUN_COMMANDS[ir.target], "```"].join("\n"),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const WORKSPACE_NOTE = [
|
|
335
|
+
"Launch the bundle from inside this directory — it reads and writes workspace state under `.crewhaus/` relative to the working directory:",
|
|
336
|
+
"",
|
|
337
|
+
"- `.crewhaus/sessions/` — session transcripts (one file per session)",
|
|
338
|
+
"- `.crewhaus/feedback/` — response ratings collected by the feedback tooling",
|
|
339
|
+
].join("\n");
|
|
340
|
+
/**
|
|
341
|
+
* Render the generated bundle README from a lowered IR. Pure; no I/O.
|
|
342
|
+
* Deterministic for a given IR (tables and env lists are sorted) so
|
|
343
|
+
* recompiles diff cleanly.
|
|
344
|
+
*/
|
|
345
|
+
export function renderBundleReadme(ir, opts = {}) {
|
|
346
|
+
const description = opts.description ?? `Compiled CrewHaus bundle for the \`${ir.target}\` target shape.`;
|
|
347
|
+
const models = collectModels(ir);
|
|
348
|
+
const harnessRows = [
|
|
349
|
+
"| | |",
|
|
350
|
+
"| --- | --- |",
|
|
351
|
+
`| Name | \`${escapeCell(ir.name)}\` |`,
|
|
352
|
+
`| Target | \`${ir.target}\` |`,
|
|
353
|
+
`| ${models.length > 1 ? "Models" : "Model"} | ${models
|
|
354
|
+
.map((m) => `\`${escapeCell(m)}\``)
|
|
355
|
+
.join(", ")} |`,
|
|
356
|
+
].join("\n");
|
|
357
|
+
const sections = [{ heading: "Harness", body: harnessRows }];
|
|
358
|
+
const tools = renderToolsSection(ir);
|
|
359
|
+
if (tools !== undefined)
|
|
360
|
+
sections.push({ heading: "Tools", body: tools });
|
|
361
|
+
const mcp = renderMcpSection(ir);
|
|
362
|
+
if (mcp !== undefined)
|
|
363
|
+
sections.push({ heading: "MCP servers", body: mcp });
|
|
364
|
+
sections.push({ heading: "Environment variables", body: renderEnvSection(ir) });
|
|
365
|
+
sections.push(opts.usage ?? defaultUsageSection(ir));
|
|
366
|
+
if (opts.includeWorkspaceNote !== false) {
|
|
367
|
+
sections.push({ heading: "Runtime data", body: WORKSPACE_NOTE });
|
|
368
|
+
}
|
|
369
|
+
sections.push(...(opts.extraSections ?? []));
|
|
370
|
+
return [
|
|
371
|
+
GENERATED_README_MARKER,
|
|
372
|
+
"",
|
|
373
|
+
`# ${ir.name}`,
|
|
374
|
+
"",
|
|
375
|
+
description,
|
|
376
|
+
"",
|
|
377
|
+
"Generated by CrewHaus — do not edit by hand; recompile from the spec instead.",
|
|
378
|
+
"",
|
|
379
|
+
...sections.flatMap((s) => [`## ${s.heading}`, "", s.body, ""]),
|
|
380
|
+
].join("\n");
|
|
381
|
+
}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction for HUMAN-READABLE renderings of spec/IR content —
|
|
3
|
+
* changelog diff lines (`spec-patch`) and generated bundle READMEs (`ir`).
|
|
4
|
+
* Two layers:
|
|
5
|
+
*
|
|
6
|
+
* 1. `isCredentialKey` — key-based: a value stored under a key that NAMES
|
|
7
|
+
* a credential (`api_key`, `botToken`, `headers`, `env`, …) is redacted
|
|
8
|
+
* wholesale, whatever the value looks like.
|
|
9
|
+
* 2. `maskCredentialTokens` — value-based: strings that are NOT under a
|
|
10
|
+
* credential key (instructions prose, command args, URLs) are scanned
|
|
11
|
+
* for well-known credential token shapes (`sk-…`, `ghp_…`, `xoxb-…`,
|
|
12
|
+
* `AKIA…`, `Bearer <token>`) plus high-length opaque tokens preceded by
|
|
13
|
+
* key-ish context words. Deliberately conservative: a bare 32-char
|
|
14
|
+
* identifier with no "key/token/secret/password" context is left alone
|
|
15
|
+
* so normal prose never gets chewed up.
|
|
16
|
+
*
|
|
17
|
+
* KEEP IN SYNC: this module is intentionally duplicated as
|
|
18
|
+
* `packages/ir/src/redact.ts` and `packages/spec-patch/src/redact.ts`.
|
|
19
|
+
* `@crewhaus/ir` keeps ZERO package dependencies (its `readme.ts` already
|
|
20
|
+
* mirrors `OUTWARD_TOOL_NAMES` from tool-builder for the same reason) and
|
|
21
|
+
* `spec-patch` is spec-layer infrastructure that must not grow an edge onto
|
|
22
|
+
* the IR layer — so neither package can host the single copy without a new
|
|
23
|
+
* dependency edge. Change one file, change both.
|
|
24
|
+
*/
|
|
25
|
+
/** Placeholder rendered in place of a value under a credential-carrying key. */
|
|
26
|
+
export declare const REDACTED_VALUE = "[redacted]";
|
|
27
|
+
/** Placeholder substituted for a credential-shaped token inside a string. */
|
|
28
|
+
export declare const MASKED_TOKEN = "***";
|
|
29
|
+
/**
|
|
30
|
+
* Whether a property key names a credential. Suffix matches require a word
|
|
31
|
+
* boundary (snake/kebab/camel) so `api_key` / `botToken` / `GITHUB_TOKEN` /
|
|
32
|
+
* `signingSecret` redact while `monkey` / `max_tokens` don't.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isCredentialKey(key: string): boolean;
|
|
35
|
+
/** A path segment / standalone word shaped like an opaque credential
|
|
36
|
+
* (Alchemy/Infura-style `/v2/<key>`): 32+ chars of token alphabet. */
|
|
37
|
+
export declare const OPAQUE_TOKEN_RE: RegExp;
|
|
38
|
+
/**
|
|
39
|
+
* Mask credential-shaped tokens inside a string. Non-credential text is
|
|
40
|
+
* returned unchanged (hit/no-hit cases are unit-tested in both packages).
|
|
41
|
+
*/
|
|
42
|
+
export declare function maskCredentialTokens(text: string): string;
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction for HUMAN-READABLE renderings of spec/IR content —
|
|
3
|
+
* changelog diff lines (`spec-patch`) and generated bundle READMEs (`ir`).
|
|
4
|
+
* Two layers:
|
|
5
|
+
*
|
|
6
|
+
* 1. `isCredentialKey` — key-based: a value stored under a key that NAMES
|
|
7
|
+
* a credential (`api_key`, `botToken`, `headers`, `env`, …) is redacted
|
|
8
|
+
* wholesale, whatever the value looks like.
|
|
9
|
+
* 2. `maskCredentialTokens` — value-based: strings that are NOT under a
|
|
10
|
+
* credential key (instructions prose, command args, URLs) are scanned
|
|
11
|
+
* for well-known credential token shapes (`sk-…`, `ghp_…`, `xoxb-…`,
|
|
12
|
+
* `AKIA…`, `Bearer <token>`) plus high-length opaque tokens preceded by
|
|
13
|
+
* key-ish context words. Deliberately conservative: a bare 32-char
|
|
14
|
+
* identifier with no "key/token/secret/password" context is left alone
|
|
15
|
+
* so normal prose never gets chewed up.
|
|
16
|
+
*
|
|
17
|
+
* KEEP IN SYNC: this module is intentionally duplicated as
|
|
18
|
+
* `packages/ir/src/redact.ts` and `packages/spec-patch/src/redact.ts`.
|
|
19
|
+
* `@crewhaus/ir` keeps ZERO package dependencies (its `readme.ts` already
|
|
20
|
+
* mirrors `OUTWARD_TOOL_NAMES` from tool-builder for the same reason) and
|
|
21
|
+
* `spec-patch` is spec-layer infrastructure that must not grow an edge onto
|
|
22
|
+
* the IR layer — so neither package can host the single copy without a new
|
|
23
|
+
* dependency edge. Change one file, change both.
|
|
24
|
+
*/
|
|
25
|
+
/** Placeholder rendered in place of a value under a credential-carrying key. */
|
|
26
|
+
export const REDACTED_VALUE = "[redacted]";
|
|
27
|
+
/** Placeholder substituted for a credential-shaped token inside a string. */
|
|
28
|
+
export const MASKED_TOKEN = "***";
|
|
29
|
+
/**
|
|
30
|
+
* Keys that carry credentials, matched case-insensitively after lowercasing.
|
|
31
|
+
* Aligned with the spec schema's credential carriers (`botToken`,
|
|
32
|
+
* `signingSecret`, `appToken`, `secretToken`, `accessToken`, `appSecret`,
|
|
33
|
+
* `retrieve.apiKey`, wallet `keyRef`) and the compiler's `lowerCredential` /
|
|
34
|
+
* `lowerWalletKeyRef` call sites — see `packages/compiler/src/index.ts` §12.
|
|
35
|
+
* `headers` and `env` are container keys: everything under them redacts.
|
|
36
|
+
*/
|
|
37
|
+
const CREDENTIAL_KEY_EXACT = new Set([
|
|
38
|
+
"key",
|
|
39
|
+
"apikey",
|
|
40
|
+
"api_key",
|
|
41
|
+
"api-key",
|
|
42
|
+
"token",
|
|
43
|
+
"secret",
|
|
44
|
+
"password",
|
|
45
|
+
"passwd",
|
|
46
|
+
"pwd",
|
|
47
|
+
"authorization",
|
|
48
|
+
"auth",
|
|
49
|
+
"credential",
|
|
50
|
+
"credentials",
|
|
51
|
+
"headers",
|
|
52
|
+
"env",
|
|
53
|
+
"keyref",
|
|
54
|
+
"key_ref",
|
|
55
|
+
"key-ref",
|
|
56
|
+
"privatekey",
|
|
57
|
+
"private_key",
|
|
58
|
+
"private-key",
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* Whether a property key names a credential. Suffix matches require a word
|
|
62
|
+
* boundary (snake/kebab/camel) so `api_key` / `botToken` / `GITHUB_TOKEN` /
|
|
63
|
+
* `signingSecret` redact while `monkey` / `max_tokens` don't.
|
|
64
|
+
*/
|
|
65
|
+
export function isCredentialKey(key) {
|
|
66
|
+
const k = key.toLowerCase();
|
|
67
|
+
if (CREDENTIAL_KEY_EXACT.has(k))
|
|
68
|
+
return true;
|
|
69
|
+
if (/[_-](?:key|token|secret|password)$/.test(k))
|
|
70
|
+
return true;
|
|
71
|
+
return /[a-z0-9](?:Key|Token|Secret|Password)$/.test(key);
|
|
72
|
+
}
|
|
73
|
+
/** A path segment / standalone word shaped like an opaque credential
|
|
74
|
+
* (Alchemy/Infura-style `/v2/<key>`): 32+ chars of token alphabet. */
|
|
75
|
+
export const OPAQUE_TOKEN_RE = /^[A-Za-z0-9_-]{32,}$/;
|
|
76
|
+
/** Well-known credential token shapes, masked wherever they appear. */
|
|
77
|
+
const TOKEN_SHAPE_RES = [
|
|
78
|
+
/\bsk-[A-Za-z0-9_-]{8,}/g, // OpenAI/Anthropic/Stripe-style secret keys
|
|
79
|
+
/\bgh[oprsu]_[A-Za-z0-9]{16,}/g, // GitHub tokens (ghp_/gho_/ghu_/ghs_/ghr_)
|
|
80
|
+
/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, // Slack tokens
|
|
81
|
+
/\bAKIA[A-Z0-9]{12,}/g, // AWS access key ids
|
|
82
|
+
];
|
|
83
|
+
/** `Bearer <token>` — the scheme word is kept, the token is masked. */
|
|
84
|
+
const BEARER_RE = /\b(bearer)\s+[A-Za-z0-9._~+/-]{8,}=*/gi;
|
|
85
|
+
/**
|
|
86
|
+
* Generic 32+-char opaque token, masked ONLY when preceded by a key-ish
|
|
87
|
+
* context word — `key: XXXX…`, `token=XXXX…` — so hashes/ids in ordinary
|
|
88
|
+
* prose survive. Group 1 (the context) is kept; the token is masked.
|
|
89
|
+
*/
|
|
90
|
+
const CONTEXTUAL_OPAQUE_RE = /\b((?:api[-_ ]?)?(?:key|token|secret|password|credential)s?\b["'\s:=-]{0,5})([A-Za-z0-9+/_-]{32,})/gi;
|
|
91
|
+
/**
|
|
92
|
+
* Mask credential-shaped tokens inside a string. Non-credential text is
|
|
93
|
+
* returned unchanged (hit/no-hit cases are unit-tested in both packages).
|
|
94
|
+
*/
|
|
95
|
+
export function maskCredentialTokens(text) {
|
|
96
|
+
let out = text;
|
|
97
|
+
for (const re of TOKEN_SHAPE_RES)
|
|
98
|
+
out = out.replace(re, MASKED_TOKEN);
|
|
99
|
+
out = out.replace(BEARER_RE, `$1 ${MASKED_TOKEN}`);
|
|
100
|
+
out = out.replace(CONTEXTUAL_OPAQUE_RE, `$1${MASKED_TOKEN}`);
|
|
101
|
+
return out;
|
|
102
|
+
}
|