@crewhaus/ir 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/dist/index.d.ts +196 -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,74 @@ 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
|
+
};
|
|
121
|
+
/** One declared candidate in a `model_pool`. */
|
|
122
|
+
export type IrModelPoolCandidate = {
|
|
123
|
+
readonly model: string;
|
|
124
|
+
readonly tags: readonly string[];
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Adaptive model routing — the N-candidate `model_pool` with a per-turn
|
|
128
|
+
* selection `policy` (`static` | `heuristic` | `learned`). Carried on the
|
|
129
|
+
* agent blocks of the routing-capable shapes (cli, channel, managed), mutually
|
|
130
|
+
* exclusive with `modelTiers`/`modelFallbacks` (enforced in the spec). Absent
|
|
131
|
+
* when the spec omits `model_pool` — codegen gates on presence so an unset
|
|
132
|
+
* block leaves bundles byte-identical. `policy` and each candidate's `tags` are
|
|
133
|
+
* always present (spec defaults them); every other knob is carried verbatim.
|
|
134
|
+
*/
|
|
135
|
+
export type IrModelPool = {
|
|
136
|
+
readonly candidates: readonly IrModelPoolCandidate[];
|
|
137
|
+
readonly policy: "static" | "heuristic" | "learned";
|
|
138
|
+
readonly objective?: {
|
|
139
|
+
readonly quality?: number;
|
|
140
|
+
readonly cost?: number;
|
|
141
|
+
readonly latency?: number;
|
|
142
|
+
};
|
|
143
|
+
readonly routing?: {
|
|
144
|
+
readonly contextTokenThreshold?: number;
|
|
145
|
+
readonly toolsToDefault?: boolean;
|
|
146
|
+
readonly firstTurnToDefault?: boolean;
|
|
147
|
+
readonly priorToolDensityThreshold?: number;
|
|
148
|
+
readonly strongTag?: string;
|
|
149
|
+
readonly cheapTag?: string;
|
|
150
|
+
};
|
|
151
|
+
readonly learning?: {
|
|
152
|
+
readonly minSamplesPerArm?: number;
|
|
153
|
+
readonly costRefUsd?: number;
|
|
154
|
+
readonly latencyRefMs?: number;
|
|
155
|
+
};
|
|
156
|
+
};
|
|
86
157
|
/**
|
|
87
158
|
* Section 55 (Track A) — named failure taxonomy. Cross-cutting; carried
|
|
88
159
|
* through to runtime-core so `recovery-engine` can consult the user's
|
|
@@ -100,10 +171,30 @@ export type IrCompaction = {
|
|
|
100
171
|
export type IrFailureTaxonomyEntry = {
|
|
101
172
|
readonly class: string;
|
|
102
173
|
readonly pattern: string;
|
|
103
|
-
|
|
174
|
+
/** Item 23 — `switch-model` reroutes onto the next provider failover
|
|
175
|
+
* candidate mid-turn (see recovery-engine). */
|
|
176
|
+
readonly recovery: "retry" | "compact" | "continue" | "tombstone" | "switch-model" | "fail";
|
|
104
177
|
readonly hint?: string;
|
|
105
178
|
};
|
|
106
179
|
export type IrFailureTaxonomy = readonly IrFailureTaxonomyEntry[];
|
|
180
|
+
/**
|
|
181
|
+
* Item 27 — run-level spend cap with a degradation ladder, lowered from the
|
|
182
|
+
* spec's `budget` block. `usdMicros` is the dollar ceiling in USD-micros
|
|
183
|
+
* (1 USD = 1_000_000) — the unit the runtime meters in. `onExceed` decides
|
|
184
|
+
* the behaviour when accrued spend reaches the cap: `stop` ends the run
|
|
185
|
+
* before the next turn; `degrade` re-resolves the primary model to `model`
|
|
186
|
+
* (one cheaper rung) and continues. Carried on the interactive shapes that
|
|
187
|
+
* loop (cli, channel, managed); absent when the spec omits the block.
|
|
188
|
+
*/
|
|
189
|
+
export type IrBudget = {
|
|
190
|
+
readonly usdMicros: number;
|
|
191
|
+
readonly onExceed: {
|
|
192
|
+
readonly kind: "stop";
|
|
193
|
+
} | {
|
|
194
|
+
readonly kind: "degrade";
|
|
195
|
+
readonly model: string;
|
|
196
|
+
};
|
|
197
|
+
};
|
|
107
198
|
/**
|
|
108
199
|
* Pillar 3 (FR-004) — per-target security fabric configuration the
|
|
109
200
|
* compiler lowers from the spec's `security` block. Today it carries the
|
|
@@ -160,8 +251,56 @@ export type IrFeedback = {
|
|
|
160
251
|
readonly location: string;
|
|
161
252
|
};
|
|
162
253
|
readonly autoDistill?: boolean;
|
|
254
|
+
/** Item 1 — gate for the CLI REPL's one-keystroke exit rating prompt.
|
|
255
|
+
* Absent → prompt (the block's presence opts in); `false` → never. */
|
|
256
|
+
readonly exitPrompt?: boolean;
|
|
163
257
|
readonly channelReactions?: boolean;
|
|
164
258
|
};
|
|
259
|
+
/**
|
|
260
|
+
* Feature #53 — cross-session memory config, lowered from `spec.memory`.
|
|
261
|
+
* Presence of the block wires Remember/Recall into the target; the auto-*
|
|
262
|
+
* switches gate auto-capture (summarize durable outcomes at teardown) and
|
|
263
|
+
* auto-recall (inject top-K memories into the system prompt at session start).
|
|
264
|
+
* Carried on the interactive shapes that run a chat loop (IrV0/cli,
|
|
265
|
+
* IrChannelV0, IrManagedV0, IrResearchV0). Absent when the spec omits `memory`.
|
|
266
|
+
*/
|
|
267
|
+
export type IrMemory = {
|
|
268
|
+
readonly enabled?: boolean;
|
|
269
|
+
readonly autoCapture?: boolean;
|
|
270
|
+
readonly autoCaptureThreshold?: number;
|
|
271
|
+
readonly autoRecall?: boolean;
|
|
272
|
+
readonly recallK?: number;
|
|
273
|
+
};
|
|
274
|
+
/** Ops item 37 — a mitigation-ladder rung the runtime SLO monitor walks on a
|
|
275
|
+
* sustained breach, in declared order. See {@link IrSlo}. */
|
|
276
|
+
export type IrSloMitigation = "alert" | "pause-intake" | "rollback";
|
|
277
|
+
/**
|
|
278
|
+
* Ops item 37 — production SLO targets + the mitigation ladder, lowered from
|
|
279
|
+
* `spec.observability.slo`. Every target is optional (declare only the ones you
|
|
280
|
+
* care about); an omitted target is never evaluated by the monitor. `windowMs`
|
|
281
|
+
* is the rolling window a breach must persist before the ladder fires (spec's
|
|
282
|
+
* `window_seconds` × 1000; the monitor defaults it when absent). `mitigation`
|
|
283
|
+
* defaults to `["alert"]` at lower time so an observe-only spec still warns.
|
|
284
|
+
* Absent from the IR when the spec omits the block.
|
|
285
|
+
*/
|
|
286
|
+
export type IrSlo = {
|
|
287
|
+
readonly errorRate?: number;
|
|
288
|
+
readonly p95LatencyMs?: number;
|
|
289
|
+
readonly ttftMs?: number;
|
|
290
|
+
readonly costPerHourUsd?: number;
|
|
291
|
+
readonly egressBlockRate?: number;
|
|
292
|
+
readonly windowMs?: number;
|
|
293
|
+
readonly mitigation: ReadonlyArray<IrSloMitigation>;
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* Ops item 37 — cross-cutting observability config, lowered from
|
|
297
|
+
* `spec.observability`. Today it carries one sub-block, `slo`. Carried on the
|
|
298
|
+
* interactive/daemon shapes that run a chat loop (IrV0/cli, IrChannelV0,
|
|
299
|
+
* IrManagedV0). Absent when the spec omits the `observability` block.
|
|
300
|
+
*/
|
|
301
|
+
export type IrObservability = {
|
|
302
|
+
readonly slo?: IrSlo;
|
|
303
|
+
};
|
|
165
304
|
/**
|
|
166
305
|
* Track F (Section 57) — typed message schemas (Σ) for multi-agent
|
|
167
306
|
* communication. Source: AgentFlow (arxiv 2604.20801). A typed graph
|
|
@@ -216,6 +355,16 @@ export type IrV0 = {
|
|
|
216
355
|
/** Model max OUTPUT tokens for one turn (spec `agent.max_tokens`).
|
|
217
356
|
* Optional; when absent the runtime default applies. */
|
|
218
357
|
readonly maxTokens?: number;
|
|
358
|
+
/** Item 22 — ordered failover models (spec `agent.model_fallbacks`).
|
|
359
|
+
* Absent when the spec omits the block; the runtime then keeps its
|
|
360
|
+
* single-adapter path. */
|
|
361
|
+
readonly modelFallbacks?: readonly string[];
|
|
362
|
+
/** Item 22 — breaker tuning (spec `agent.circuit_breaker`). */
|
|
363
|
+
readonly circuitBreaker?: IrCircuitBreaker;
|
|
364
|
+
/** Item 26 — two-tier turn-difficulty router. Absent → single-model. */
|
|
365
|
+
readonly modelTiers?: IrModelTiers;
|
|
366
|
+
/** Adaptive model routing — N-candidate pool. Absent → single-model. */
|
|
367
|
+
readonly modelPool?: IrModelPool;
|
|
219
368
|
};
|
|
220
369
|
readonly tools: readonly string[];
|
|
221
370
|
readonly toolConfigs: IrToolConfigs;
|
|
@@ -226,12 +375,19 @@ export type IrV0 = {
|
|
|
226
375
|
readonly cli?: IrCliOptions;
|
|
227
376
|
/** Section 55 (Track A) — named failure taxonomy. Optional. */
|
|
228
377
|
readonly failureTaxonomy?: IrFailureTaxonomy;
|
|
378
|
+
/** Item 27 — run-level spend cap + degradation ladder. Optional. */
|
|
379
|
+
readonly budget?: IrBudget;
|
|
229
380
|
/** Pillar 3 (FR-004) — security fabric config (intent-gate judge
|
|
230
381
|
* selection). Optional; absent when the spec omits the `security`
|
|
231
382
|
* block. */
|
|
232
383
|
readonly security?: IrSecurity;
|
|
233
384
|
/** Response-feedback config. Optional; absent when the spec omits `feedback`. */
|
|
234
385
|
readonly feedback?: IrFeedback;
|
|
386
|
+
/** #53 cross-session memory config. Optional; absent when the spec omits `memory`. */
|
|
387
|
+
readonly memory?: IrMemory;
|
|
388
|
+
/** Ops item 37 — SLO targets + mitigation ladder. Optional; absent when the
|
|
389
|
+
* spec omits the `observability` block. */
|
|
390
|
+
readonly observability?: IrObservability;
|
|
235
391
|
/** §47 cross-cutting blockchain subsystem (slice 0). All optional. */
|
|
236
392
|
readonly chains?: readonly IrChainBinding[];
|
|
237
393
|
readonly wallets?: readonly IrWalletBinding[];
|
|
@@ -463,6 +619,14 @@ export type IrChannelV0 = {
|
|
|
463
619
|
readonly agent: {
|
|
464
620
|
readonly model: string;
|
|
465
621
|
readonly instructions: string;
|
|
622
|
+
/** Item 22 — ordered failover models (spec `agent.model_fallbacks`). */
|
|
623
|
+
readonly modelFallbacks?: readonly string[];
|
|
624
|
+
/** Item 22 — breaker tuning (spec `agent.circuit_breaker`). */
|
|
625
|
+
readonly circuitBreaker?: IrCircuitBreaker;
|
|
626
|
+
/** Item 26 — two-tier turn-difficulty router. Absent → single-model. */
|
|
627
|
+
readonly modelTiers?: IrModelTiers;
|
|
628
|
+
/** Adaptive model routing — N-candidate pool. Absent → single-model. */
|
|
629
|
+
readonly modelPool?: IrModelPool;
|
|
466
630
|
};
|
|
467
631
|
readonly tools: readonly string[];
|
|
468
632
|
readonly toolConfigs: IrToolConfigs;
|
|
@@ -476,9 +640,16 @@ export type IrChannelV0 = {
|
|
|
476
640
|
readonly gateway?: IrChannelGateway;
|
|
477
641
|
/** Section 55 (Track A) — named failure taxonomy. Optional. */
|
|
478
642
|
readonly failureTaxonomy?: IrFailureTaxonomy;
|
|
643
|
+
/** Item 27 — run-level spend cap + degradation ladder. Optional. */
|
|
644
|
+
readonly budget?: IrBudget;
|
|
479
645
|
/** Response-feedback config. `feedback.channelReactions` gates Slack 👍/👎
|
|
480
646
|
* → user_feedback codegen in this target. Absent when spec omits it. */
|
|
481
647
|
readonly feedback?: IrFeedback;
|
|
648
|
+
/** #53 cross-session memory config. Optional; absent when the spec omits `memory`. */
|
|
649
|
+
readonly memory?: IrMemory;
|
|
650
|
+
/** Ops item 37 — SLO targets + mitigation ladder. Optional; absent when the
|
|
651
|
+
* spec omits the `observability` block. */
|
|
652
|
+
readonly observability?: IrObservability;
|
|
482
653
|
/** §47 cross-cutting blockchain subsystem (slice 0). All optional. */
|
|
483
654
|
readonly chains?: readonly IrChainBinding[];
|
|
484
655
|
readonly wallets?: readonly IrWalletBinding[];
|
|
@@ -505,12 +676,28 @@ export type IrManagedV0 = {
|
|
|
505
676
|
readonly agent: {
|
|
506
677
|
readonly model: string;
|
|
507
678
|
readonly instructions: string;
|
|
679
|
+
/** Item 22 — ordered failover models (spec `agent.model_fallbacks`). */
|
|
680
|
+
readonly modelFallbacks?: readonly string[];
|
|
681
|
+
/** Item 22 — breaker tuning (spec `agent.circuit_breaker`). */
|
|
682
|
+
readonly circuitBreaker?: IrCircuitBreaker;
|
|
683
|
+
/** Item 26 — two-tier turn-difficulty router. Absent → single-model. */
|
|
684
|
+
readonly modelTiers?: IrModelTiers;
|
|
685
|
+
/** Adaptive model routing — N-candidate pool. Absent → single-model. */
|
|
686
|
+
readonly modelPool?: IrModelPool;
|
|
508
687
|
};
|
|
509
688
|
readonly tenants: readonly IrManagedTenant[];
|
|
510
689
|
readonly permissions: IrPermissions;
|
|
511
690
|
readonly compaction: IrCompaction;
|
|
512
691
|
/** Section 55 (Track A) — named failure taxonomy. Optional. */
|
|
513
692
|
readonly failureTaxonomy?: IrFailureTaxonomy;
|
|
693
|
+
/** Item 27 — run-level spend cap + degradation ladder. Optional. */
|
|
694
|
+
readonly budget?: IrBudget;
|
|
695
|
+
/** #53 cross-session memory config. Optional; absent when the spec omits `memory`. */
|
|
696
|
+
readonly memory?: IrMemory;
|
|
697
|
+
/** Ops item 37 — SLO targets + mitigation ladder. Optional; absent when the
|
|
698
|
+
* spec omits the `observability` block. The managed daemon's `pause-intake`
|
|
699
|
+
* rung reuses its `budget_exceeded` 429 path. */
|
|
700
|
+
readonly observability?: IrObservability;
|
|
514
701
|
};
|
|
515
702
|
/**
|
|
516
703
|
* Section 19 — Graph IR. A `target: "graph"` spec lowers into a fixed
|
|
@@ -700,6 +887,8 @@ export type IrResearchV0 = {
|
|
|
700
887
|
readonly compaction: IrCompaction;
|
|
701
888
|
/** Section 55 (Track A) — named failure taxonomy. Optional. */
|
|
702
889
|
readonly failureTaxonomy?: IrFailureTaxonomy;
|
|
890
|
+
/** #53 cross-session memory config. Optional; absent when the spec omits `memory`. */
|
|
891
|
+
readonly memory?: IrMemory;
|
|
703
892
|
/** §47 cross-cutting blockchain subsystem (slice 0). All optional. */
|
|
704
893
|
readonly chains?: readonly IrChainBinding[];
|
|
705
894
|
readonly wallets?: readonly IrWalletBinding[];
|
|
@@ -969,3 +1158,4 @@ export type Bundle = {
|
|
|
969
1158
|
readonly content: string;
|
|
970
1159
|
}>;
|
|
971
1160
|
};
|
|
1161
|
+
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
|
+
}
|