@intentius/chant 0.19.1 → 0.20.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/build-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +54 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +210 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +226 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +102 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +81 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +119 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +61 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +485 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1026 -0
- package/src/discovery/fold-import.ts +1805 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +233 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +535 -0
- package/src/fold/fold.ts +648 -0
- package/src/fold/subset-doc-parity.test.ts +183 -0
- package/src/fold/subset.test.ts +241 -0
- package/src/fold/subset.ts +302 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +14 -0
- package/src/lexicon-schema.ts +3 -1
- package/src/lexicon.ts +82 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { realpathSync, rmSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import type { Declarable } from "../../declarable";
|
|
5
|
+
import { DiscoveryError, type DiscoveryErrorType } from "../../errors";
|
|
6
|
+
import { decodeEntitySet, type EntitySetWire } from "../entity-wire-codec";
|
|
7
|
+
import { bundleDriver } from "./bundle";
|
|
8
|
+
import { classifyChildError } from "./child-errors";
|
|
9
|
+
import { generateDriverSource } from "./driver";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* chant #1045 Phase 2 — runs every run-fallback file for a build TOGETHER, as
|
|
13
|
+
* one bundled module graph, inside one sandboxed child process, and returns
|
|
14
|
+
* the same shape `discover()`'s own in-process run path would have produced:
|
|
15
|
+
* a named, ref-resolved entities map plus any errors.
|
|
16
|
+
*
|
|
17
|
+
* Isolation mechanics (verified on Node v24.13.1 — see the chant#1045 PR
|
|
18
|
+
* description for the full write-up):
|
|
19
|
+
* - `--permission --allow-fs-read=<bundle dir>,<project dir>[,<trusted
|
|
20
|
+
* external package dirs>]` — no filesystem write, no child-process, no
|
|
21
|
+
* worker-thread access. Bundling with esbuild first (not a packaging
|
|
22
|
+
* change — see `./bundle.ts`) means the child needs NO TypeScript loader
|
|
23
|
+
* (no `tsx`, so no `--allow-worker` and no writable temp dir either),
|
|
24
|
+
* unlike the plain `tsx`-based run path. The "trusted external package
|
|
25
|
+
* dirs" allowance is narrow and specific: `./bundle.ts` deliberately
|
|
26
|
+
* leaves a couple of chant/lexicon-internal dependencies (`typescript`)
|
|
27
|
+
* unbundled and resolves them to their real, fixed location instead —
|
|
28
|
+
* project source never controls what's installed there.
|
|
29
|
+
* - The env is a spawn-time scrub (`env: {}` below, plus `PATH` — see the
|
|
30
|
+
* option below), not `--permission`: Node's Permission Model does not gate
|
|
31
|
+
* `process.env` at all (confirmed: every key stays readable even under
|
|
32
|
+
* `--permission`).
|
|
33
|
+
* - Network egress is NOT addressed here — Node has no flag for it. See the
|
|
34
|
+
* chant#1045 PR description / docs for the residual-risk statement and
|
|
35
|
+
* deployment guidance (a container with no egress, a network namespace).
|
|
36
|
+
* This function does not claim to close that gap.
|
|
37
|
+
*
|
|
38
|
+
* What does NOT run inside the child: fold (`tryFoldFile`, `../fold-import`)
|
|
39
|
+
* stays exactly where it is today, in the parent, unsandboxed — fold already
|
|
40
|
+
* executes zero of the file's own top-level code (chant #1022/#1023), so
|
|
41
|
+
* isolating it buys nothing and would only cost a bundle+spawn per build.
|
|
42
|
+
* Only genuine run-fallback files are handed to this function.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export interface SandboxRunResult {
|
|
46
|
+
/** Named, ref-resolved entities from the run-fallback set — decoded from the child's `EntitySetWire` response, functionally indistinguishable from what `importModule` + `collectEntities` + `resolveAttrRefs` would have produced in-process for this same file set (see `../entity-wire.ts`'s `decodeEntitySet` doc). */
|
|
47
|
+
entities: Map<string, Declarable>;
|
|
48
|
+
/** Import/collection/resolution/permission errors, already chant-shaped (see `./child-errors.ts` — a permission denial names the file and the operation, never a raw `ERR_ACCESS_DENIED`). */
|
|
49
|
+
errors: DiscoveryError[];
|
|
50
|
+
/** esbuild bundling wall-clock time — chant#1045 asks this be measured, not silently accepted. */
|
|
51
|
+
bundleMs: number;
|
|
52
|
+
/** Bundle size in bytes. */
|
|
53
|
+
bundleBytes: number;
|
|
54
|
+
/**
|
|
55
|
+
* Entity name → declaring file, for every entity in {@link entities} whose
|
|
56
|
+
* provenance (`../../provenance.ts`) named one. The wire format itself
|
|
57
|
+
* doesn't carry this (`encodeEntitySet` intentionally drops build metadata,
|
|
58
|
+
* not declared configuration) — it rides back as a small side channel so a
|
|
59
|
+
* parent-side merge collision against the fold-only set (a bare name
|
|
60
|
+
* genuinely exported by both a folded and a run-fallback file in the same
|
|
61
|
+
* directory) can name the real file, not the entity name. See `../index.ts`.
|
|
62
|
+
*/
|
|
63
|
+
provenanceByName: Record<string, string>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ChildResponse {
|
|
67
|
+
entitySet: EntitySetWire;
|
|
68
|
+
errors: Array<{ name: string; file: string; message: string; type: DiscoveryErrorType }>;
|
|
69
|
+
provenanceByName?: Record<string, string>;
|
|
70
|
+
fatal?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** How long to wait for the sandboxed child to report back before treating it as hung and killing it. Generous: bundling+import+collect for a whole build's run-fallback set should be a small multiple of what the equivalent in-process run takes. */
|
|
74
|
+
const CHILD_TIMEOUT_MS = 120_000;
|
|
75
|
+
|
|
76
|
+
function isChildResponse(value: unknown): value is ChildResponse {
|
|
77
|
+
return typeof value === "object" && value !== null && "entitySet" in value && "errors" in value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Run `files` (already decided "run" by `discover()`'s fold/taint pass)
|
|
82
|
+
* together, isolated, in one sandboxed child process. Returns the same
|
|
83
|
+
* `{ entities, errors }` shape the parent's own `collectEntities` +
|
|
84
|
+
* `resolveAttrRefs` would have — see `../index.ts`'s `discover()`, which
|
|
85
|
+
* merges this result into the entities it collected from folded files.
|
|
86
|
+
*
|
|
87
|
+
* @param files - Absolute paths to run-fallback files.
|
|
88
|
+
* @param buildRoot - The directory `discover()` was pointed at — threaded to
|
|
89
|
+
* `collectEntities` inside the child (stack-prefix disambiguation, #932)
|
|
90
|
+
* and used to compute the project-directory read allowance.
|
|
91
|
+
*/
|
|
92
|
+
export async function runFallbackFilesSandboxed(
|
|
93
|
+
files: readonly string[],
|
|
94
|
+
buildRoot: string,
|
|
95
|
+
): Promise<SandboxRunResult> {
|
|
96
|
+
if (files.length === 0) {
|
|
97
|
+
return { entities: new Map(), errors: [], bundleMs: 0, bundleBytes: 0, provenanceByName: {} };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const driverSource = generateDriverSource({ files, buildRoot });
|
|
101
|
+
const { bundlePath, bundleDir, externalReadPaths, durationMs, bytes } = await bundleDriver(driverSource);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
let projectRealpath: string;
|
|
105
|
+
try {
|
|
106
|
+
projectRealpath = realpathSync(resolve(buildRoot));
|
|
107
|
+
} catch {
|
|
108
|
+
projectRealpath = resolve(buildRoot);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const response = await runChildProcess(bundlePath, bundleDir, projectRealpath, externalReadPaths);
|
|
112
|
+
|
|
113
|
+
const errors = (response.errors ?? []).map(
|
|
114
|
+
(e) => new DiscoveryError(e.file, e.message, e.type),
|
|
115
|
+
);
|
|
116
|
+
const provenanceByName = response.provenanceByName ?? {};
|
|
117
|
+
if (response.fatal) {
|
|
118
|
+
return { entities: new Map(), errors, bundleMs: durationMs, bundleBytes: bytes, provenanceByName };
|
|
119
|
+
}
|
|
120
|
+
const entities = decodeEntitySet(response.entitySet ?? { entities: [] });
|
|
121
|
+
return { entities, errors, bundleMs: durationMs, bundleBytes: bytes, provenanceByName };
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return {
|
|
124
|
+
entities: new Map(),
|
|
125
|
+
errors: [classifyChildError("", err, "import")],
|
|
126
|
+
provenanceByName: {},
|
|
127
|
+
bundleMs: durationMs,
|
|
128
|
+
bundleBytes: bytes,
|
|
129
|
+
};
|
|
130
|
+
} finally {
|
|
131
|
+
rmSync(bundleDir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Fork the bundle under `--permission`, with a scrubbed environment, and resolve with its one IPC message (or reject on crash/timeout/fork error). */
|
|
136
|
+
function runChildProcess(
|
|
137
|
+
bundlePath: string,
|
|
138
|
+
bundleDir: string,
|
|
139
|
+
projectRealpath: string,
|
|
140
|
+
externalReadPaths: readonly string[],
|
|
141
|
+
): Promise<ChildResponse> {
|
|
142
|
+
return new Promise((resolvePromise, reject) => {
|
|
143
|
+
const readAllowances = [bundleDir, projectRealpath, ...externalReadPaths].map(
|
|
144
|
+
(p) => `--allow-fs-read=${p}`,
|
|
145
|
+
);
|
|
146
|
+
const child = fork(bundlePath, [], {
|
|
147
|
+
execArgv: ["--permission", ...readAllowances],
|
|
148
|
+
// chant #1045 Phase 2 — Node's Permission Model does not gate
|
|
149
|
+
// `process.env`; scrubbing it here is the only way to keep the ambient
|
|
150
|
+
// environment out of untrusted project source's reach. `PATH` is kept
|
|
151
|
+
// only because some platforms' module resolution/dynamic linking
|
|
152
|
+
// consults it; it carries no project secrets.
|
|
153
|
+
env: { PATH: process.env.PATH ?? "" },
|
|
154
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let settled = false;
|
|
158
|
+
let stderrBuf = "";
|
|
159
|
+
|
|
160
|
+
const timeout = setTimeout(() => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
settled = true;
|
|
163
|
+
child.kill();
|
|
164
|
+
reject(new Error(`sandboxed run timed out after ${CHILD_TIMEOUT_MS}ms`));
|
|
165
|
+
}, CHILD_TIMEOUT_MS);
|
|
166
|
+
|
|
167
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
168
|
+
stderrBuf += chunk.toString();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
child.on("message", (msg: unknown) => {
|
|
172
|
+
if (settled || !isChildResponse(msg)) return;
|
|
173
|
+
settled = true;
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
resolvePromise(msg);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
child.on("error", (err) => {
|
|
179
|
+
if (settled) return;
|
|
180
|
+
settled = true;
|
|
181
|
+
clearTimeout(timeout);
|
|
182
|
+
reject(err);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
child.on("exit", (code, signal) => {
|
|
186
|
+
if (settled) return;
|
|
187
|
+
settled = true;
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
reject(
|
|
190
|
+
new Error(
|
|
191
|
+
`sandboxed child exited before reporting results (code ${code}, signal ${signal})${stderrBuf.trim() ? `: ${stderrBuf.trim()}` : ""}`,
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
* Error types for chant discovery, build, and lint failures
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* `"permission"` (chant #1045 Phase 2) — a run-fallback file executing inside
|
|
7
|
+
* the sandboxed child (`./discovery/sandbox/`) hit Node's Permission Model
|
|
8
|
+
* (`ERR_ACCESS_DENIED`) — filesystem, child-process, or worker-thread access
|
|
9
|
+
* outside the sandbox's allowlist. Distinct from `"import"` (a module simply
|
|
10
|
+
* failed to load/execute for an ordinary reason) so a permission denial is
|
|
11
|
+
* recognizable as a sandbox boundary hit, not a generic import failure.
|
|
12
|
+
*/
|
|
13
|
+
export type DiscoveryErrorType = "import" | "resolution" | "circular" | "permission";
|
|
6
14
|
|
|
7
15
|
/**
|
|
8
16
|
* Error during file discovery or module import
|