knodin 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/bin/cli.js +108 -1859
- package/dist/bin/launcher.js +3 -10
- package/dist/chunks/chunk-DMQAGX77.js +654 -0
- package/dist/chunks/chunk-F4Z3Z766.js +4 -0
- package/dist/chunks/chunk-SIJAQVSX.js +3 -0
- package/dist/chunks/chunk-X6M4HUUE.js +2 -0
- package/dist/chunks/chunk-YPRMY2LP.js +8 -0
- package/dist/chunks/pure-compression-cli-4TA2TQD5.js +5 -0
- package/dist/chunks/server-7EDF4CBY.js +14 -0
- package/dist/chunks/structural-fast-path-KD5KQSPX.js +4 -0
- package/dist/src/init-progress-worker.js +2 -106
- package/dist/src/repair-progress-worker.js +2 -120
- package/docs/INSTALLATION.md +17 -1
- package/docs/releases/0.7.6.md +25 -0
- package/package.json +1 -110
- package/dist/src/agent-integration.js +0 -238
- package/dist/src/artifact-refresh.js +0 -81
- package/dist/src/cli-args.js +0 -274
- package/dist/src/cli-model.js +0 -358
- package/dist/src/codeflow-replay.js +0 -80
- package/dist/src/compact-structural.js +0 -96
- package/dist/src/competitive-cold-mcp.js +0 -40
- package/dist/src/competitive-constraints.js +0 -20
- package/dist/src/competitive-manifest.js +0 -411
- package/dist/src/competitive-measurement.js +0 -183
- package/dist/src/competitive-runner.js +0 -487
- package/dist/src/competitive-sandbox.js +0 -108
- package/dist/src/context-export.js +0 -422
- package/dist/src/context.js +0 -102
- package/dist/src/diagnostics.js +0 -449
- package/dist/src/docs-sections.js +0 -141
- package/dist/src/doctor.js +0 -380
- package/dist/src/engine/ann-hnsw.js +0 -271
- package/dist/src/engine/embeddings.js +0 -193
- package/dist/src/engine/file-walker.js +0 -43
- package/dist/src/engine/git-history.js +0 -289
- package/dist/src/engine/index.js +0 -13377
- package/dist/src/engine/perf.js +0 -115
- package/dist/src/engine/prune.js +0 -112
- package/dist/src/engine/scip-import.js +0 -408
- package/dist/src/engine/source-policy.js +0 -69
- package/dist/src/engine/sqlite.js +0 -71
- package/dist/src/engine/symbol-delete.js +0 -58
- package/dist/src/execution-profile.js +0 -203
- package/dist/src/failure-diagnosis.js +0 -649
- package/dist/src/fleet.js +0 -7
- package/dist/src/git-executable.js +0 -31
- package/dist/src/graph-query-health.js +0 -115
- package/dist/src/hook-manager-integration.js +0 -156
- package/dist/src/index-activity.js +0 -125
- package/dist/src/init-progress.js +0 -155
- package/dist/src/init.js +0 -1254
- package/dist/src/lifecycle-health.js +0 -250
- package/dist/src/lsp-readonly.js +0 -217
- package/dist/src/output-compression.js +0 -629
- package/dist/src/output-telemetry.js +0 -363
- package/dist/src/pr-triage.js +0 -638
- package/dist/src/progressive-evidence.js +0 -473
- package/dist/src/pure-compression-cli.js +0 -101
- package/dist/src/relationship-adapters.js +0 -370
- package/dist/src/release-attestation.js +0 -533
- package/dist/src/release-preflight.js +0 -510
- package/dist/src/repair-progress.js +0 -262
- package/dist/src/repository-init-process.js +0 -173
- package/dist/src/repository-management.js +0 -1231
- package/dist/src/response-budget.js +0 -194
- package/dist/src/server.js +0 -73
- package/dist/src/structural-fast-path.js +0 -338
- package/dist/src/structural-snapshot.js +0 -33
- package/dist/src/system-config.js +0 -615
- package/dist/src/terminal-help.js +0 -83
- package/dist/src/tools/knodin-tools.js +0 -1529
- package/dist/src/update-ceremony.js +0 -158
- package/dist/src/update-policy.js +0 -944
- package/dist/src/update-trust.js +0 -503
- package/dist/src/version.js +0 -13
- package/dist/src/visualization.js +0 -162
- package/dist/src/wait-for-fresh.js +0 -98
- package/dist/src/worktree-lifecycle.js +0 -231
- package/docs/CONTAINED-EXECUTION.md +0 -77
- package/docs/DIAGNOSTICS.md +0 -45
- package/docs/GIT-HISTORY-REVIEW.md +0 -39
- package/docs/PROGRESSIVE-EVIDENCE.md +0 -37
- package/docs/PT-ACCESS-RECOMMENDATION.md +0 -91
- package/docs/RELEASE-0.3-EVIDENCE.md +0 -73
- package/docs/SCIP-IMPORT.md +0 -57
- package/docs/SIGNED-UPDATES.md +0 -151
- package/docs/TELEMETRY.md +0 -46
- package/docs/assets/knodin-favicon.svg +0 -4
- package/docs/releases/0.3.0.md +0 -46
- package/docs/releases/0.4.0.md +0 -68
- package/docs/releases/0.4.1.md +0 -28
- package/docs/releases/0.4.2.md +0 -27
- package/docs/releases/0.4.3.md +0 -23
- package/docs/releases/0.5.0.md +0 -29
- package/docs/releases/0.5.1.md +0 -17
- package/docs/releases/0.6.0.md +0 -18
- package/docs/releases/0.7.0.md +0 -24
- package/docs/releases/0.7.1.md +0 -21
- package/docs/releases/0.7.2.md +0 -21
- package/docs/releases/0.7.3.md +0 -23
- package/docs/releases/0.7.4.md +0 -17
- package/schemas/release-attestation-v1.schema.json +0 -210
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { isIndexablePath } from "./prune.js";
|
|
3
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
4
|
-
".ts",
|
|
5
|
-
".tsx",
|
|
6
|
-
".mts",
|
|
7
|
-
".cts",
|
|
8
|
-
".js",
|
|
9
|
-
".jsx",
|
|
10
|
-
".mjs",
|
|
11
|
-
".cjs",
|
|
12
|
-
".py",
|
|
13
|
-
".cls",
|
|
14
|
-
".trigger",
|
|
15
|
-
".page",
|
|
16
|
-
".component",
|
|
17
|
-
".app",
|
|
18
|
-
".cmp",
|
|
19
|
-
".design",
|
|
20
|
-
".docs",
|
|
21
|
-
".evt",
|
|
22
|
-
".intf",
|
|
23
|
-
".svg",
|
|
24
|
-
".sql",
|
|
25
|
-
".pkb",
|
|
26
|
-
".pks",
|
|
27
|
-
".prisma",
|
|
28
|
-
".java",
|
|
29
|
-
".cs",
|
|
30
|
-
".go",
|
|
31
|
-
".rs",
|
|
32
|
-
".clp",
|
|
33
|
-
".ws",
|
|
34
|
-
".xml",
|
|
35
|
-
".tf",
|
|
36
|
-
".tfvars",
|
|
37
|
-
".hcl",
|
|
38
|
-
".dockerfile",
|
|
39
|
-
".lsif",
|
|
40
|
-
]);
|
|
41
|
-
function isSalesforceBundleMarkup(normalized) {
|
|
42
|
-
return (/^force-app\/main\/default\/lwc\/([A-Za-z][A-Za-z0-9_]*)\/\1\.(?:html|css)$/.test(normalized) ||
|
|
43
|
-
/^force-app\/main\/default\/aura\/([A-Za-z][A-Za-z0-9_]*)\/\1\.(?:css)$/.test(normalized));
|
|
44
|
-
}
|
|
45
|
-
function isSalesforceExperienceJson(normalized) {
|
|
46
|
-
return (normalized === "lwr.config.json" ||
|
|
47
|
-
/^force-app\/main\/default\/experiences\/[A-Za-z][A-Za-z0-9_]*\/(?:routes|views|themes)\/[^/]+\.json$/.test(normalized));
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Canonical, path-only candidate policy shared by full-repository collection,
|
|
51
|
-
* file watching, repair selection, and Git-hook refresh. Content-sensitive
|
|
52
|
-
* formats (currently dbt manifest.json) are accepted as candidates here and
|
|
53
|
-
* validated by the parser before symbols are persisted.
|
|
54
|
-
*/
|
|
55
|
-
export function isIndexableSourcePath(relativePath) {
|
|
56
|
-
if (!isIndexablePath(relativePath))
|
|
57
|
-
return false;
|
|
58
|
-
const normalized = relativePath.replaceAll("\\", "/");
|
|
59
|
-
const base = path.posix.basename(normalized);
|
|
60
|
-
if (base === "Dockerfile")
|
|
61
|
-
return true;
|
|
62
|
-
if (normalized.endsWith(".json")) {
|
|
63
|
-
return base === "manifest.json" || isSalesforceExperienceJson(normalized);
|
|
64
|
-
}
|
|
65
|
-
if (normalized.endsWith(".html") || normalized.endsWith(".css")) {
|
|
66
|
-
return isSalesforceBundleMarkup(normalized);
|
|
67
|
-
}
|
|
68
|
-
return SOURCE_EXTENSIONS.has(path.posix.extname(normalized));
|
|
69
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Production SQLite adapter for the Node runtime.
|
|
3
|
-
*
|
|
4
|
-
* knodin's engine was originally written against the compact `bun:sqlite`
|
|
5
|
-
* surface. Node 24 ships `node:sqlite` without a runtime flag, so this adapter
|
|
6
|
-
* preserves the engine's existing typed `query<Row, Params>()` contract while
|
|
7
|
-
* using Node's native `DatabaseSync` implementation underneath.
|
|
8
|
-
*/
|
|
9
|
-
import { DatabaseSync } from "node:sqlite";
|
|
10
|
-
/**
|
|
11
|
-
* knodin's call sites pass bind params either spread (`stmt.all(a, b)`) or as a
|
|
12
|
-
* single array (`db.run(sql, [a, b])`). node:sqlite wants them spread — normalize a
|
|
13
|
-
* lone array argument back into a spread list. SQLite has no array column type,
|
|
14
|
-
* so a single array can only ever mean "the parameter list", never one value.
|
|
15
|
-
*/
|
|
16
|
-
function normalize(params) {
|
|
17
|
-
if (params.length === 1 && Array.isArray(params[0]))
|
|
18
|
-
return params[0];
|
|
19
|
-
return params;
|
|
20
|
-
}
|
|
21
|
-
class Statement {
|
|
22
|
-
// node:sqlite's parameter union is intentionally narrower than the engine's
|
|
23
|
-
// generic call-site contract. Runtime validation still belongs to SQLite.
|
|
24
|
-
// biome-ignore lint/suspicious/noExplicitAny: adapter boundary around native bindings.
|
|
25
|
-
stmt;
|
|
26
|
-
// biome-ignore lint/suspicious/noExplicitAny: adapter boundary around native bindings.
|
|
27
|
-
constructor(stmt) {
|
|
28
|
-
this.stmt = stmt;
|
|
29
|
-
}
|
|
30
|
-
all(...params) {
|
|
31
|
-
return this.stmt.all(...normalize(params));
|
|
32
|
-
}
|
|
33
|
-
get(...params) {
|
|
34
|
-
return (this.stmt.get(...normalize(params)) ?? null);
|
|
35
|
-
}
|
|
36
|
-
run(...params) {
|
|
37
|
-
return this.stmt.run(...normalize(params));
|
|
38
|
-
}
|
|
39
|
-
finalize() {
|
|
40
|
-
// node:sqlite statements are released on GC; there is nothing to finalize.
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
export class Database {
|
|
44
|
-
db;
|
|
45
|
-
constructor(databasePath, options = {}) {
|
|
46
|
-
this.db = new DatabaseSync(databasePath, { readOnly: options.readonly === true });
|
|
47
|
-
}
|
|
48
|
-
query(sql) {
|
|
49
|
-
return new Statement(this.db.prepare(sql));
|
|
50
|
-
}
|
|
51
|
-
prepare(sql) {
|
|
52
|
-
return new Statement(this.db.prepare(sql));
|
|
53
|
-
}
|
|
54
|
-
run(sql, ...params) {
|
|
55
|
-
if (params.length === 0) {
|
|
56
|
-
// DDL / PRAGMA / BEGIN / COMMIT / multi-statement schema — `exec` avoids
|
|
57
|
-
// prepare() edge cases and handles more-than-one statement in one string.
|
|
58
|
-
this.db.exec(sql);
|
|
59
|
-
return { changes: 0, lastInsertRowid: 0 };
|
|
60
|
-
}
|
|
61
|
-
// biome-ignore lint/suspicious/noExplicitAny: adapter boundary around native bindings.
|
|
62
|
-
const statement = this.db.prepare(sql);
|
|
63
|
-
return statement.run(...normalize(params));
|
|
64
|
-
}
|
|
65
|
-
exec(sql) {
|
|
66
|
-
this.db.exec(sql);
|
|
67
|
-
}
|
|
68
|
-
close() {
|
|
69
|
-
this.db.close();
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single source of truth for "delete symbols, and everything keyed off them" (R17).
|
|
3
|
-
*
|
|
4
|
-
* `symbol_embeddings.symbolId` declares
|
|
5
|
-
* `REFERENCES symbols(id) ON DELETE CASCADE`, but SQLite disables foreign-key
|
|
6
|
-
* enforcement per connection by default, so for the life of the project that
|
|
7
|
-
* cascade never fired: `symbols.id` is AUTOINCREMENT, every re-index of a file
|
|
8
|
-
* deletes its symbols and reinserts them under NEW ids, and the embeddings
|
|
9
|
-
* pointing at the old ids were stranded forever. On `/path/to/large-repository` that had
|
|
10
|
-
* reached 37,258 orphans against 18,629 live symbols — 67% of the table, all
|
|
11
|
-
* of it scanned by semantic search's exact cosine pass.
|
|
12
|
-
*
|
|
13
|
-
* `getOrInitDb` now sets `PRAGMA foreign_keys = ON`, which makes the declared
|
|
14
|
-
* cascade real. These helpers are the second layer: they delete the embeddings
|
|
15
|
-
* EXPLICITLY, so correctness does not depend on a per-connection pragma that
|
|
16
|
-
* some future connection path forgets to set. Every `DELETE FROM symbols` site
|
|
17
|
-
* in the engine goes through here — the R14 lesson (one policy, many thin
|
|
18
|
-
* adapters) applied to deletion instead of ignore rules.
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
|
-
* Shared predicate for "this embedding's symbol no longer exists" — the
|
|
22
|
-
* single source of truth for both the DELETE below and the `stats` orphan
|
|
23
|
-
* COUNT in `src/engine/index.ts`, which previously hardcoded the identical
|
|
24
|
-
* SQL as an independent string literal.
|
|
25
|
-
*/
|
|
26
|
-
export const ORPHANED_EMBEDDING_PREDICATE = "symbolId NOT IN (SELECT id FROM symbols)";
|
|
27
|
-
/**
|
|
28
|
-
* Purge embeddings whose symbol no longer exists.
|
|
29
|
-
*
|
|
30
|
-
* Idempotent and cheap — a single correlated DELETE. Used by the 13 → 14
|
|
31
|
-
* migration to clean indexes polluted before the cascade worked, and safe to
|
|
32
|
-
* call on an empty or freshly-created table.
|
|
33
|
-
*
|
|
34
|
-
* Deliberately does NOT touch live embeddings: re-embedding a real corpus is
|
|
35
|
-
* ~40 minutes of CPU, so this must stay a purge and never become a wipe.
|
|
36
|
-
*/
|
|
37
|
-
export function purgeOrphanEmbeddings(db) {
|
|
38
|
-
db.run(`DELETE FROM symbol_embeddings WHERE ${ORPHANED_EMBEDDING_PREDICATE}`);
|
|
39
|
-
}
|
|
40
|
-
/** Delete every symbol declared in one repo-relative file, plus its embeddings. */
|
|
41
|
-
export function deleteSymbolsForFile(db, filePath) {
|
|
42
|
-
db.run("DELETE FROM symbol_embeddings WHERE symbolId IN (SELECT id FROM symbols WHERE filePath = ?)", [filePath]);
|
|
43
|
-
db.run("DELETE FROM symbols WHERE filePath = ?", [filePath]);
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Delete every symbol whose file path matches a LIKE pattern, plus its
|
|
47
|
-
* embeddings. Used by the dbt path, whose symbols live under synthetic
|
|
48
|
-
* `.knodin/dbt/%` paths rather than one real file.
|
|
49
|
-
*/
|
|
50
|
-
export function deleteSymbolsMatchingPath(db, likePattern) {
|
|
51
|
-
db.run("DELETE FROM symbol_embeddings WHERE symbolId IN (SELECT id FROM symbols WHERE filePath LIKE ?)", [likePattern]);
|
|
52
|
-
db.run("DELETE FROM symbols WHERE filePath LIKE ?", [likePattern]);
|
|
53
|
-
}
|
|
54
|
-
/** Delete every symbol in the index, plus every embedding. Full re-index only. */
|
|
55
|
-
export function deleteAllSymbols(db) {
|
|
56
|
-
db.run("DELETE FROM symbol_embeddings;");
|
|
57
|
-
db.run("DELETE FROM symbols;");
|
|
58
|
-
}
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import childProcess from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { compressOutput } from "./output-compression.js";
|
|
6
|
-
function defaultGlobalConfigPath() {
|
|
7
|
-
const base = process.env.XDG_CONFIG_HOME
|
|
8
|
-
? path.resolve(process.env.XDG_CONFIG_HOME)
|
|
9
|
-
: path.join(os.homedir(), ".config");
|
|
10
|
-
return path.join(base, "knodin", "execution.json");
|
|
11
|
-
}
|
|
12
|
-
function readDocument(file) {
|
|
13
|
-
try {
|
|
14
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function sandboxLiteral(value) {
|
|
21
|
-
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
22
|
-
}
|
|
23
|
-
function macosPolicy(repo, _executable) {
|
|
24
|
-
const protectedRoots = ["/Users", "/Volumes", "/Network", "/private/tmp", "/private/var/folders"];
|
|
25
|
-
return [
|
|
26
|
-
"(version 1)",
|
|
27
|
-
"(allow default)",
|
|
28
|
-
"(deny network*)",
|
|
29
|
-
"(deny process-fork)",
|
|
30
|
-
`(deny file-read* ${protectedRoots.map((item) => `(subpath ${sandboxLiteral(item)})`).join(" ")})`,
|
|
31
|
-
"(deny file-write*)",
|
|
32
|
-
`(allow file-read* file-write* (subpath ${sandboxLiteral(repo)}) (literal "/dev/null"))`,
|
|
33
|
-
].join(" ");
|
|
34
|
-
}
|
|
35
|
-
function unavailable(profile, warning) {
|
|
36
|
-
return {
|
|
37
|
-
schemaVersion: 1,
|
|
38
|
-
status: "unavailable",
|
|
39
|
-
profile,
|
|
40
|
-
exit: { code: null, signal: null },
|
|
41
|
-
containment: {
|
|
42
|
-
level: "containment-unavailable",
|
|
43
|
-
platform: process.platform,
|
|
44
|
-
adapter: "unavailable",
|
|
45
|
-
filesystem: "unavailable",
|
|
46
|
-
network: "unavailable",
|
|
47
|
-
processes: "unavailable",
|
|
48
|
-
warning,
|
|
49
|
-
},
|
|
50
|
-
limits: { timeoutMs: 0, maxOutputBytes: 0, maxProcesses: 1 },
|
|
51
|
-
output: null,
|
|
52
|
-
audit: { commandTextRecorded: false, profileRecorded: true },
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
function validateProfile(profileName, value) {
|
|
56
|
-
if (!value || typeof value !== "object")
|
|
57
|
-
throw new Error(`execution profile ${profileName} is invalid`);
|
|
58
|
-
const profile = value;
|
|
59
|
-
if (!path.isAbsolute(profile.executable ?? ""))
|
|
60
|
-
throw new Error(`execution profile ${profileName}: executable must be absolute`);
|
|
61
|
-
if (!Array.isArray(profile.args) ||
|
|
62
|
-
profile.args.some((arg) => typeof arg !== "string" || arg.includes("\0")))
|
|
63
|
-
throw new Error(`execution profile ${profileName}: args must be a NUL-free string array`);
|
|
64
|
-
if (profile.network !== "deny")
|
|
65
|
-
throw new Error(`execution profile ${profileName}: network must be deny`);
|
|
66
|
-
if (!Number.isInteger(profile.timeoutMs) ||
|
|
67
|
-
(profile.timeoutMs ?? 0) < 1 ||
|
|
68
|
-
(profile.timeoutMs ?? 0) > 300_000)
|
|
69
|
-
throw new Error(`execution profile ${profileName}: timeoutMs must be 1..300000`);
|
|
70
|
-
if (!Number.isInteger(profile.maxOutputBytes) ||
|
|
71
|
-
(profile.maxOutputBytes ?? 0) < 256 ||
|
|
72
|
-
(profile.maxOutputBytes ?? 0) > 16 * 1024 * 1024)
|
|
73
|
-
throw new Error(`execution profile ${profileName}: maxOutputBytes must be 256..16777216`);
|
|
74
|
-
if (profile.maxProcesses !== 1)
|
|
75
|
-
throw new Error(`execution profile ${profileName}: maxProcesses must be 1 for hard fork denial`);
|
|
76
|
-
if (profile.containment !== undefined && profile.containment !== "fully-contained")
|
|
77
|
-
throw new Error(`execution profile ${profileName}: only fully-contained is supported`);
|
|
78
|
-
return profile;
|
|
79
|
-
}
|
|
80
|
-
function appendAudit(repo, record) {
|
|
81
|
-
const destination = path.join(repo, ".knodin", "execution-audit.jsonl");
|
|
82
|
-
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
83
|
-
fs.appendFileSync(destination, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
84
|
-
}
|
|
85
|
-
export async function runExecutionProfile(options) {
|
|
86
|
-
const repo = fs.realpathSync(options.repo);
|
|
87
|
-
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(options.profile))
|
|
88
|
-
throw new Error("execution profile identity must match [a-z][a-z0-9_-]{0,63}");
|
|
89
|
-
const globalDocument = readDocument(options.globalConfigPath ?? defaultGlobalConfigPath());
|
|
90
|
-
const repositoryDocument = readDocument(path.join(repo, ".knodin", "execution.json"));
|
|
91
|
-
if (globalDocument?.execution?.enabled !== true)
|
|
92
|
-
return unavailable(options.profile, "execution is disabled globally");
|
|
93
|
-
if (repositoryDocument?.execution?.enabled !== true)
|
|
94
|
-
return unavailable(options.profile, "execution is disabled for this repository");
|
|
95
|
-
const configured = repositoryDocument.execution.profiles?.[options.profile];
|
|
96
|
-
if (!configured)
|
|
97
|
-
throw new Error(`execution profile not found: ${options.profile}`);
|
|
98
|
-
const profile = validateProfile(options.profile, configured);
|
|
99
|
-
if (process.platform !== "darwin" || !fs.existsSync("/usr/bin/sandbox-exec"))
|
|
100
|
-
return unavailable(options.profile, "no certified containment adapter is available on this platform");
|
|
101
|
-
const executable = fs.realpathSync(profile.executable);
|
|
102
|
-
const temporaryDirectory = fs.mkdtempSync(path.join(repo, ".knodin", "execution-tmp-"));
|
|
103
|
-
const environment = {
|
|
104
|
-
PATH: `${path.dirname(executable)}:/usr/bin:/bin`,
|
|
105
|
-
LANG: "C.UTF-8",
|
|
106
|
-
LC_ALL: "C.UTF-8",
|
|
107
|
-
TMPDIR: temporaryDirectory,
|
|
108
|
-
};
|
|
109
|
-
const policy = macosPolicy(repo, executable);
|
|
110
|
-
const proc = childProcess.spawn("/usr/bin/sandbox-exec", ["-p", policy, executable, ...profile.args], {
|
|
111
|
-
cwd: repo,
|
|
112
|
-
env: environment,
|
|
113
|
-
shell: false,
|
|
114
|
-
detached: true,
|
|
115
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
116
|
-
});
|
|
117
|
-
let stdout = Buffer.alloc(0);
|
|
118
|
-
let stderr = Buffer.alloc(0);
|
|
119
|
-
let reason = "exit";
|
|
120
|
-
let stopped = false;
|
|
121
|
-
const stop = (next) => {
|
|
122
|
-
if (stopped || reason !== "exit")
|
|
123
|
-
return;
|
|
124
|
-
reason = next;
|
|
125
|
-
if (proc.pid)
|
|
126
|
-
try {
|
|
127
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
128
|
-
}
|
|
129
|
-
catch { }
|
|
130
|
-
};
|
|
131
|
-
const append = (stream, chunk) => {
|
|
132
|
-
const remaining = Math.max(0, profile.maxOutputBytes - stdout.length - stderr.length);
|
|
133
|
-
const accepted = chunk.subarray(0, remaining);
|
|
134
|
-
if (stream === "stdout")
|
|
135
|
-
stdout = Buffer.concat([stdout, accepted]);
|
|
136
|
-
else
|
|
137
|
-
stderr = Buffer.concat([stderr, accepted]);
|
|
138
|
-
if (accepted.length < chunk.length)
|
|
139
|
-
stop("output-limit");
|
|
140
|
-
};
|
|
141
|
-
proc.stdout.on("data", (chunk) => append("stdout", chunk));
|
|
142
|
-
proc.stderr.on("data", (chunk) => append("stderr", chunk));
|
|
143
|
-
const timer = setTimeout(() => stop("timeout"), profile.timeoutMs);
|
|
144
|
-
const closed = await new Promise((resolve, reject) => {
|
|
145
|
-
proc.once("error", reject);
|
|
146
|
-
proc.once("close", (code, signal) => resolve({ code, signal }));
|
|
147
|
-
});
|
|
148
|
-
stopped = true;
|
|
149
|
-
clearTimeout(timer);
|
|
150
|
-
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
151
|
-
const combined = [
|
|
152
|
-
...(stdout.length ? [{ stream: "stdout", text: stdout.toString("utf8") }] : []),
|
|
153
|
-
...(stderr.length ? [{ stream: "stderr", text: stderr.toString("utf8") }] : []),
|
|
154
|
-
];
|
|
155
|
-
const output = compressOutput(repo, {
|
|
156
|
-
events: combined,
|
|
157
|
-
exitCode: closed.code,
|
|
158
|
-
signal: closed.signal,
|
|
159
|
-
lineBudget: 200,
|
|
160
|
-
byteBudget: Math.min(profile.maxOutputBytes, 16_384),
|
|
161
|
-
retain: true,
|
|
162
|
-
});
|
|
163
|
-
const finalReason = reason;
|
|
164
|
-
const status = finalReason === "timeout"
|
|
165
|
-
? "timed-out"
|
|
166
|
-
: finalReason === "output-limit"
|
|
167
|
-
? "output-limit"
|
|
168
|
-
: closed.code === 0
|
|
169
|
-
? "completed"
|
|
170
|
-
: "failed";
|
|
171
|
-
const result = {
|
|
172
|
-
schemaVersion: 1,
|
|
173
|
-
status,
|
|
174
|
-
profile: options.profile,
|
|
175
|
-
exit: closed,
|
|
176
|
-
containment: {
|
|
177
|
-
level: "fully-contained",
|
|
178
|
-
platform: process.platform,
|
|
179
|
-
adapter: "macos-sandbox-exec",
|
|
180
|
-
filesystem: "repository-write-user-data-read-bounded",
|
|
181
|
-
network: "deny",
|
|
182
|
-
processes: "fork-denied",
|
|
183
|
-
warning: "macOS sandbox-exec is deprecated. Writes are repository-bound; system runtime paths remain readable while user homes, mounted volumes, network paths, and shared temporary roots are read-denied outside the repository.",
|
|
184
|
-
},
|
|
185
|
-
limits: {
|
|
186
|
-
timeoutMs: profile.timeoutMs,
|
|
187
|
-
maxOutputBytes: profile.maxOutputBytes,
|
|
188
|
-
maxProcesses: 1,
|
|
189
|
-
},
|
|
190
|
-
output,
|
|
191
|
-
audit: { commandTextRecorded: false, profileRecorded: true },
|
|
192
|
-
};
|
|
193
|
-
appendAudit(repo, {
|
|
194
|
-
timestamp: new Date().toISOString(),
|
|
195
|
-
profile: options.profile,
|
|
196
|
-
status,
|
|
197
|
-
exitCode: closed.code,
|
|
198
|
-
signal: closed.signal,
|
|
199
|
-
containment: result.containment.level,
|
|
200
|
-
outputArtifactId: output.artifact.id,
|
|
201
|
-
});
|
|
202
|
-
return result;
|
|
203
|
-
}
|