@opensip-cli/checks-typescript 0.1.7 → 0.1.9
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 +2 -2
- package/dist/__tests__/branch-fixtures-3.test.js +41 -0
- package/dist/__tests__/branch-fixtures-3.test.js.map +1 -1
- package/dist/__tests__/branch-fixtures.test.js +1 -3
- package/dist/__tests__/branch-fixtures.test.js.map +1 -1
- package/dist/__tests__/command-handler-host-owned-output.test.d.ts +2 -0
- package/dist/__tests__/command-handler-host-owned-output.test.d.ts.map +1 -0
- package/dist/__tests__/command-handler-host-owned-output.test.js +85 -0
- package/dist/__tests__/command-handler-host-owned-output.test.js.map +1 -0
- package/dist/__tests__/host-tool-runtime-import-boundary.test.js +69 -12
- package/dist/__tests__/host-tool-runtime-import-boundary.test.js.map +1 -1
- package/dist/__tests__/null-safety-typed.test.d.ts +2 -0
- package/dist/__tests__/null-safety-typed.test.d.ts.map +1 -0
- package/dist/__tests__/null-safety-typed.test.js +112 -0
- package/dist/__tests__/null-safety-typed.test.js.map +1 -0
- package/dist/__tests__/shared-program.test.d.ts +2 -0
- package/dist/__tests__/shared-program.test.d.ts.map +1 -0
- package/dist/__tests__/shared-program.test.js +41 -0
- package/dist/__tests__/shared-program.test.js.map +1 -0
- package/dist/__tests__/shipped-allowlists-are-generic.test.d.ts +2 -0
- package/dist/__tests__/shipped-allowlists-are-generic.test.d.ts.map +1 -0
- package/dist/__tests__/shipped-allowlists-are-generic.test.js +64 -0
- package/dist/__tests__/shipped-allowlists-are-generic.test.js.map +1 -0
- package/dist/checks/architecture/command-handler-host-owned-output.d.ts +25 -0
- package/dist/checks/architecture/command-handler-host-owned-output.d.ts.map +1 -0
- package/dist/checks/architecture/command-handler-host-owned-output.js +165 -0
- package/dist/checks/architecture/command-handler-host-owned-output.js.map +1 -0
- package/dist/checks/architecture/host-tool-runtime-import-boundary.d.ts +24 -7
- package/dist/checks/architecture/host-tool-runtime-import-boundary.d.ts.map +1 -1
- package/dist/checks/architecture/host-tool-runtime-import-boundary.js +114 -33
- package/dist/checks/architecture/host-tool-runtime-import-boundary.js.map +1 -1
- package/dist/checks/architecture/index.d.ts +30 -0
- package/dist/checks/architecture/index.d.ts.map +1 -1
- package/dist/checks/architecture/index.js +30 -0
- package/dist/checks/architecture/index.js.map +1 -1
- package/dist/checks/quality/code-structure/duplicate-utility-functions.d.ts +13 -1
- package/dist/checks/quality/code-structure/duplicate-utility-functions.d.ts.map +1 -1
- package/dist/checks/quality/code-structure/duplicate-utility-functions.js +7 -3
- package/dist/checks/quality/code-structure/duplicate-utility-functions.js.map +1 -1
- package/dist/checks/quality/data-integrity/null-safety.d.ts +63 -2
- package/dist/checks/quality/data-integrity/null-safety.d.ts.map +1 -1
- package/dist/checks/quality/data-integrity/null-safety.js +191 -102
- package/dist/checks/quality/data-integrity/null-safety.js.map +1 -1
- package/dist/checks/quality/patterns/result-pattern-consistency.d.ts +21 -0
- package/dist/checks/quality/patterns/result-pattern-consistency.d.ts.map +1 -1
- package/dist/checks/quality/patterns/result-pattern-consistency.js +16 -15
- package/dist/checks/quality/patterns/result-pattern-consistency.js.map +1 -1
- package/dist/checks/resilience/detached-promises.d.ts.map +1 -1
- package/dist/checks/resilience/detached-promises.js +4 -0
- package/dist/checks/resilience/detached-promises.js.map +1 -1
- package/dist/checks/resilience/no-raw-fetch.d.ts +14 -0
- package/dist/checks/resilience/no-raw-fetch.d.ts.map +1 -1
- package/dist/checks/resilience/no-raw-fetch.js +7 -7
- package/dist/checks/resilience/no-raw-fetch.js.map +1 -1
- package/dist/display/architecture.d.ts.map +1 -1
- package/dist/display/architecture.js +1 -0
- package/dist/display/architecture.js.map +1 -1
- package/dist/shared/type-program.d.ts +23 -0
- package/dist/shared/type-program.d.ts.map +1 -0
- package/dist/shared/type-program.js +35 -0
- package/dist/shared/type-program.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { RunScope, runWithScope } from '@opensip-cli/core';
|
|
5
|
+
import { fileCache, setCurrentRecipeCheckConfig } from '@opensip-cli/fitness';
|
|
6
|
+
import { createTypeCheckedProgram } from '@opensip-cli/lang-typescript';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
+
import { analyzeNullSafetyTyped, nullSafety, } from '../checks/quality/data-integrity/null-safety.js';
|
|
9
|
+
let dir;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
dir = mkdtempSync(join(tmpdir(), 'opensip-nstyped-'));
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
fileCache.clear();
|
|
15
|
+
rmSync(dir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
function write(rel, content) {
|
|
18
|
+
const abs = join(dir, rel);
|
|
19
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
20
|
+
writeFileSync(abs, content);
|
|
21
|
+
return abs;
|
|
22
|
+
}
|
|
23
|
+
/** A scope carrying the fitness fileCache + a fresh shared-Program cell. */
|
|
24
|
+
function scopeWithFitness() {
|
|
25
|
+
const scope = new RunScope();
|
|
26
|
+
Object.assign(scope, { fitness: { fileCache, tsProgram: { value: undefined } } });
|
|
27
|
+
return scope;
|
|
28
|
+
}
|
|
29
|
+
// One fixture exercising every decision: nullable/undefined call receivers
|
|
30
|
+
// (flag), non-null receiver (ok), `any` receiver (fail-open), optional chain
|
|
31
|
+
// (skip), and a nullable element-access receiver (flag).
|
|
32
|
+
const FIXTURE = [
|
|
33
|
+
'function getNullable(): { x: number } | null { return null; }',
|
|
34
|
+
'function getMaybe(): { y: number } | undefined { return undefined; }',
|
|
35
|
+
'function getSafe(): { z: number } { return { z: 1 }; }',
|
|
36
|
+
'function getAny(): any { return 1; }',
|
|
37
|
+
'const arr: ({ a: number } | null)[] = [];',
|
|
38
|
+
'export function f(): unknown[] {',
|
|
39
|
+
' const out: unknown[] = [];',
|
|
40
|
+
' out.push(getNullable().x);',
|
|
41
|
+
' out.push(getMaybe().y);',
|
|
42
|
+
' out.push(getSafe().z);',
|
|
43
|
+
' out.push(getAny().w);',
|
|
44
|
+
' out.push(getNullable()?.x ?? 0);',
|
|
45
|
+
' out.push(arr[0].a);',
|
|
46
|
+
' return out;',
|
|
47
|
+
'}',
|
|
48
|
+
].join('\n');
|
|
49
|
+
describe('analyzeNullSafetyTyped (type-aware detector)', () => {
|
|
50
|
+
it('flags only receivers whose actual type includes null/undefined', () => {
|
|
51
|
+
const file = write('src/sample.ts', FIXTURE);
|
|
52
|
+
const { checker, getSourceFile } = createTypeCheckedProgram([file], { projectRoot: dir });
|
|
53
|
+
const found = analyzeNullSafetyTyped(getSourceFile(file), checker, file).map((v) => v.match);
|
|
54
|
+
expect(found).toEqual(expect.arrayContaining(['getNullable().x', 'getMaybe().y', 'arr[0].a']));
|
|
55
|
+
expect(found).toHaveLength(3);
|
|
56
|
+
// Non-null, `any` (fail-open), and optional-chained receivers are NOT flagged.
|
|
57
|
+
expect(found).not.toContain('getSafe().z');
|
|
58
|
+
expect(found).not.toContain('getAny().w');
|
|
59
|
+
expect(found.some((m) => m?.includes('?.'))).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
it('skips safe-by-construction paths (schema/DI) entirely', () => {
|
|
62
|
+
const file = write('src/sample.ts', FIXTURE);
|
|
63
|
+
const { checker, getSourceFile } = createTypeCheckedProgram([file], { projectRoot: dir });
|
|
64
|
+
// The filePath drives the path skip independently of the SourceFile content.
|
|
65
|
+
expect(analyzeNullSafetyTyped(getSourceFile(file), checker, '/proj/src/schema/x.ts')).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
it('honors additionalSafeBuilders as a manual escape hatch for unresolved symbols', async () => {
|
|
68
|
+
const file = write('src/sample.ts', FIXTURE);
|
|
69
|
+
const { checker, getSourceFile } = createTypeCheckedProgram([file], { projectRoot: dir });
|
|
70
|
+
const sf = getSourceFile(file);
|
|
71
|
+
const scope = new RunScope();
|
|
72
|
+
await runWithScope(scope, () => {
|
|
73
|
+
setCurrentRecipeCheckConfig(scope, {
|
|
74
|
+
'null-safety': { additionalSafeBuilders: ['getNullable('] },
|
|
75
|
+
});
|
|
76
|
+
const found = analyzeNullSafetyTyped(sf, checker, file).map((v) => v.match);
|
|
77
|
+
expect(found).not.toContain('getNullable().x'); // suppressed by escape hatch
|
|
78
|
+
expect(found).toContain('getMaybe().y'); // still flagged
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('null-safety check — analyzeAll mode selection', () => {
|
|
84
|
+
it('defaults to type-aware — catches nullable-return accesses the convention misses', async () => {
|
|
85
|
+
const file = write('src/svc.ts', FIXTURE);
|
|
86
|
+
const scope = scopeWithFitness();
|
|
87
|
+
await runWithScope(scope, async () => {
|
|
88
|
+
// No typeAware config → default on.
|
|
89
|
+
await fileCache.prewarm(dir, ['**/*']);
|
|
90
|
+
const result = await nullSafety.run(dir, { targetFiles: [file] });
|
|
91
|
+
expect(result.signals.length).toBeGreaterThanOrEqual(3);
|
|
92
|
+
expect(result.signals.every((s) => s.message.includes('unsafe property access'))).toBe(true);
|
|
93
|
+
// The `get*`-prefixed nullable returns are caught (convention would miss them).
|
|
94
|
+
expect(result.signals.some((s) => s.message.includes("'.x'"))).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
it('falls back to the convention heuristic when typeAware is disabled', async () => {
|
|
98
|
+
const file = write('src/svc.ts', FIXTURE);
|
|
99
|
+
const scope = scopeWithFitness();
|
|
100
|
+
await runWithScope(scope, async () => {
|
|
101
|
+
setCurrentRecipeCheckConfig(scope, { 'null-safety': { typeAware: false } });
|
|
102
|
+
await fileCache.prewarm(dir, ['**/*']);
|
|
103
|
+
const result = await nullSafety.run(dir, { targetFiles: [file] });
|
|
104
|
+
// Convention trusts `get*` calls as non-null (its false-negative), so the
|
|
105
|
+
// nullable-return accesses are missed; the unknown element-access receiver
|
|
106
|
+
// (`arr[0]`) is still flagged.
|
|
107
|
+
expect(result.signals.some((s) => s.message.includes("'.x'"))).toBe(false);
|
|
108
|
+
expect(result.signals.some((s) => s.message.includes("'.a'"))).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=null-safety-typed.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"null-safety-typed.test.js","sourceRoot":"","sources":["../../src/__tests__/null-safety-typed.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-program.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/shared-program.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { RunScope, runWithScope } from '@opensip-cli/core';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { getSharedTypeCheckedProgram } from '../shared/type-program.js';
|
|
7
|
+
let dir;
|
|
8
|
+
let file;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
dir = mkdtempSync(join(tmpdir(), 'opensip-shared-prog-'));
|
|
11
|
+
file = join(dir, 'a.ts');
|
|
12
|
+
writeFileSync(file, 'export const x: string | null = null;\n');
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
rmSync(dir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
describe('getSharedTypeCheckedProgram', () => {
|
|
18
|
+
it('builds the Program once per run and reuses it across calls (memoized on the subscope cell)', async () => {
|
|
19
|
+
const scope = new RunScope();
|
|
20
|
+
// Minimal fitness subscope: the helper only reads `fitness.tsProgram`.
|
|
21
|
+
Object.assign(scope, { fitness: { tsProgram: { value: undefined } } });
|
|
22
|
+
await runWithScope(scope, () => {
|
|
23
|
+
const first = getSharedTypeCheckedProgram([file]);
|
|
24
|
+
const second = getSharedTypeCheckedProgram([file]);
|
|
25
|
+
expect(second).toBe(first); // same instance → built exactly once per run
|
|
26
|
+
return Promise.resolve();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it('builds a fresh, uncached Program when the run has no fitness subscope cell', async () => {
|
|
30
|
+
// A bare scope (no fitness subscope) overrides any ambient test scope; with
|
|
31
|
+
// no cell to memoize into, each call builds a fresh Program.
|
|
32
|
+
const scope = new RunScope();
|
|
33
|
+
await runWithScope(scope, () => {
|
|
34
|
+
const a = getSharedTypeCheckedProgram([file]);
|
|
35
|
+
const b = getSharedTypeCheckedProgram([file]);
|
|
36
|
+
expect(a).not.toBe(b);
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=shared-program.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-program.test.js","sourceRoot":"","sources":["../../src/__tests__/shared-program.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAErE,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAExE,IAAI,GAAW,CAAC;AAChB,IAAI,IAAY,CAAC;AAEjB,UAAU,CAAC,GAAG,EAAE;IACd,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,sBAAsB,CAAC,CAAC,CAAC;IAC1D,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACzB,aAAa,CAAC,IAAI,EAAE,yCAAyC,CAAC,CAAC;AACjE,CAAC,CAAC,CAAC;AACH,SAAS,CAAC,GAAG,EAAE;IACb,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAChD,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,EAAE,CAAC,4FAA4F,EAAE,KAAK,IAAI,EAAE;QAC1G,MAAM,KAAK,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC7B,uEAAuE;QACvE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC;QACvE,MAAM,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE;YAC7B,MAAM,KAAK,GAAG,2BAA2B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAClD,MAAM,MAAM,GAAG,2BAA2B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YACnD,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,6CAA6C;YACzE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4EAA4E,EAAE,KAAK,IAAI,EAAE;QAC1F,4EAA4E;QAC5E,6DAA6D;QAC7D,MAAM,KAAK,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE;YAC7B,MAAM,CAAC,GAAG,2BAA2B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAC9C,MAAM,CAAC,GAAG,2BAA2B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAC9C,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACtB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shipped-allowlists-are-generic.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/shipped-allowlists-are-generic.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview De-leak guard (b): the hardcoded allowlists shipped inside the
|
|
3
|
+
* generic @opensip-cli/checks-* packs must contain only language/library-generic
|
|
4
|
+
* entries — never project-specific symbols carried over from the private
|
|
5
|
+
* codebase this tool was extracted from. A leaked safe-symbol silently
|
|
6
|
+
* suppresses real findings in any adopter that happens to use that name.
|
|
7
|
+
*
|
|
8
|
+
* Companion to the source-text guard (a) in
|
|
9
|
+
* opensip-cli/fit/checks/shipped-checks-must-be-generic.mjs. This one inspects
|
|
10
|
+
* the live exported data structures, so it catches a leak the moment it is added
|
|
11
|
+
* to an array — independent of the dogfood run.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { DOMAIN_SPECIFIC_FUNCTION_NAMES } from '../checks/quality/code-structure/duplicate-utility-functions.js';
|
|
15
|
+
import { SAFE_BUILDER_PREFIXES, SAFE_METHOD_PREFIXES, } from '../checks/quality/data-integrity/null-safety.js';
|
|
16
|
+
import { THROW_ALLOWED_PATHS } from '../checks/quality/patterns/result-pattern-consistency.js';
|
|
17
|
+
/**
|
|
18
|
+
* Distinctive project-specific identifiers / brand tokens from the original
|
|
19
|
+
* (pre-open-source) private codebase. None may appear in ANY shipped allowlist;
|
|
20
|
+
* adopters supply their own via recipe config (`additionalSafeBuilders`, etc.).
|
|
21
|
+
*
|
|
22
|
+
* Deliberately limited to UNAMBIGUOUS identifiers: generic-sounding names that
|
|
23
|
+
* legitimately appear in some allowlists (`getConfig`, `getLogger` are valid
|
|
24
|
+
* duplicate-name exemptions in DOMAIN_SPECIFIC_FUNCTION_NAMES) are NOT listed, so
|
|
25
|
+
* this stays false-positive-free.
|
|
26
|
+
*/
|
|
27
|
+
const FOREIGN_ALLOWLIST_TOKEN = /Escrow|I18nError|getSqlite|getDatabase|getRegistry|getSync|getTenantId|TypedEventBus|CredentialConfig|ContextManager|stripThinkTags|getNumberFormatter|getDateFormatter|formatRelative|ensureError|extractErrorMessage|chronoswap|sanitizeForPrompt/i;
|
|
28
|
+
/**
|
|
29
|
+
* The only bare (non-member, non-call) identifiers allowed in
|
|
30
|
+
* SAFE_BUILDER_PREFIXES: documented library entry points. Every other entry
|
|
31
|
+
* must be member-qualified (`Object.`, `db.`) or a constructor/call (`new URL(`,
|
|
32
|
+
* `prepare(`). A bare camelCase identifier is almost always a project-specific
|
|
33
|
+
* getter and belongs in recipe config, not the shipped pack.
|
|
34
|
+
*/
|
|
35
|
+
const REVIEWED_BARE_LIBRARY_BUILDERS = new Set(['createQueryBuilder', 'getRepository']); // TypeORM
|
|
36
|
+
function asStrings(entries) {
|
|
37
|
+
return [...entries].map((e) => (e instanceof RegExp ? e.source : e));
|
|
38
|
+
}
|
|
39
|
+
describe('shipped allowlists are generic (de-leak guard b)', () => {
|
|
40
|
+
// Count gate: any add/remove to the shipped safe-builder allowlist trips this,
|
|
41
|
+
// forcing the author to bump the count AND have a reviewer confirm the new
|
|
42
|
+
// entry is genuinely generic (not a project-specific symbol that belongs in
|
|
43
|
+
// an adopter's `additionalSafeBuilders` recipe config).
|
|
44
|
+
it('SAFE_BUILDER_PREFIXES holds exactly the reviewed number of generic entries', () => {
|
|
45
|
+
expect(SAFE_BUILDER_PREFIXES).toHaveLength(54);
|
|
46
|
+
});
|
|
47
|
+
it('every SAFE_BUILDER_PREFIXES entry is a language/library construct, not a bare project getter', () => {
|
|
48
|
+
const bareLeaks = SAFE_BUILDER_PREFIXES.filter((e) => !e.includes('.') && !e.includes('(') && !REVIEWED_BARE_LIBRARY_BUILDERS.has(e));
|
|
49
|
+
expect(bareLeaks).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
it('no shipped allowlist contains a foreign project-specific identifier', () => {
|
|
52
|
+
const arrays = {
|
|
53
|
+
SAFE_BUILDER_PREFIXES: asStrings(SAFE_BUILDER_PREFIXES),
|
|
54
|
+
SAFE_METHOD_PREFIXES: asStrings(SAFE_METHOD_PREFIXES),
|
|
55
|
+
THROW_ALLOWED_PATHS: asStrings(THROW_ALLOWED_PATHS),
|
|
56
|
+
DOMAIN_SPECIFIC_FUNCTION_NAMES: asStrings(DOMAIN_SPECIFIC_FUNCTION_NAMES),
|
|
57
|
+
};
|
|
58
|
+
for (const [name, entries] of Object.entries(arrays)) {
|
|
59
|
+
const leaked = entries.filter((e) => FOREIGN_ALLOWLIST_TOKEN.test(e));
|
|
60
|
+
expect(leaked, `${name} must not contain foreign-domain symbols`).toEqual([]);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=shipped-allowlists-are-generic.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shipped-allowlists-are-generic.test.js","sourceRoot":"","sources":["../../src/__tests__/shipped-allowlists-are-generic.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,8BAA8B,EAAE,MAAM,iEAAiE,CAAC;AACjH,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0DAA0D,CAAC;AAE/F;;;;;;;;;GASG;AACH,MAAM,uBAAuB,GAC3B,sPAAsP,CAAC;AAEzP;;;;;;GAMG;AACH,MAAM,8BAA8B,GAAG,IAAI,GAAG,CAAC,CAAC,oBAAoB,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,UAAU;AAEnG,SAAS,SAAS,CAAC,OAAkC;IACnD,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,QAAQ,CAAC,kDAAkD,EAAE,GAAG,EAAE;IAChE,+EAA+E;IAC/E,2EAA2E;IAC3E,4EAA4E;IAC5E,wDAAwD;IACxD,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,MAAM,CAAC,qBAAqB,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8FAA8F,EAAE,GAAG,EAAE;QACtG,MAAM,SAAS,GAAG,qBAAqB,CAAC,MAAM,CAC5C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,8BAA8B,CAAC,GAAG,CAAC,CAAC,CAAC,CACtF,CAAC;QACF,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,MAAM,GAA6B;YACvC,qBAAqB,EAAE,SAAS,CAAC,qBAAqB,CAAC;YACvD,oBAAoB,EAAE,SAAS,CAAC,oBAAoB,CAAC;YACrD,mBAAmB,EAAE,SAAS,CAAC,mBAAmB,CAAC;YACnD,8BAA8B,EAAE,SAAS,CAAC,8BAA8B,CAAC;SAC1E,CAAC;QACF,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACrD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YACtE,MAAM,CAAC,MAAM,EAAE,GAAG,IAAI,0CAA0C,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAChF,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tool command handlers must let the host own output and exit.
|
|
3
|
+
*
|
|
4
|
+
* A Tool plugin declares its commands as `CommandSpec`s via `defineCommand({...})`
|
|
5
|
+
* and the host mounts them — owning rendering, `--json`, and the exit code. The
|
|
6
|
+
* handler receives a host context (`cli`) and must route its results through that
|
|
7
|
+
* seam: it returns a result/envelope and (when it must influence the exit) calls
|
|
8
|
+
* `cli.setExitCode(...)`. A handler that writes run output straight to stdout
|
|
9
|
+
* (`process.stdout.write` / `console.log`), or terminates the process itself
|
|
10
|
+
* (`process.exit`), bypasses the host: the output never reaches a formatter, never
|
|
11
|
+
* honours `--json`, and the process can exit before pending delivery drains.
|
|
12
|
+
*
|
|
13
|
+
* The escape hatch is DECLARED, not implicit: a command that genuinely owns its
|
|
14
|
+
* own output surface (a completion script, a file export, subprocess IPC) sets
|
|
15
|
+
* `output: 'raw-stream'` with a `rawStreamReason`. This check therefore fires only
|
|
16
|
+
* inside a `defineCommand({...})` whose `output` is NOT `'raw-stream'` — exactly
|
|
17
|
+
* where the host-owned-output contract applies. It is generic: it keys on the
|
|
18
|
+
* public command-authoring vocabulary every tool author uses, with no host path
|
|
19
|
+
* gating, so an adopter's own `fit` run enforces the same seam.
|
|
20
|
+
*/
|
|
21
|
+
import { type CheckViolation } from '@opensip-cli/fitness';
|
|
22
|
+
/** Pure analysis over a parsed source file. Exported for unit tests. */
|
|
23
|
+
export declare function analyzeCommandHandlerHostOwnedOutput(content: string, filePath: string): CheckViolation[];
|
|
24
|
+
export declare const commandHandlerHostOwnedOutput: import("@opensip-cli/fitness").Check;
|
|
25
|
+
//# sourceMappingURL=command-handler-host-owned-output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-handler-host-owned-output.d.ts","sourceRoot":"","sources":["../../../src/checks/architecture/command-handler-host-owned-output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAqHpF,wEAAwE;AACxE,wBAAgB,oCAAoC,CAClD,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,cAAc,EAAE,CA0BlB;AAED,eAAO,MAAM,6BAA6B,sCAcxC,CAAC"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tool command handlers must let the host own output and exit.
|
|
3
|
+
*
|
|
4
|
+
* A Tool plugin declares its commands as `CommandSpec`s via `defineCommand({...})`
|
|
5
|
+
* and the host mounts them — owning rendering, `--json`, and the exit code. The
|
|
6
|
+
* handler receives a host context (`cli`) and must route its results through that
|
|
7
|
+
* seam: it returns a result/envelope and (when it must influence the exit) calls
|
|
8
|
+
* `cli.setExitCode(...)`. A handler that writes run output straight to stdout
|
|
9
|
+
* (`process.stdout.write` / `console.log`), or terminates the process itself
|
|
10
|
+
* (`process.exit`), bypasses the host: the output never reaches a formatter, never
|
|
11
|
+
* honours `--json`, and the process can exit before pending delivery drains.
|
|
12
|
+
*
|
|
13
|
+
* The escape hatch is DECLARED, not implicit: a command that genuinely owns its
|
|
14
|
+
* own output surface (a completion script, a file export, subprocess IPC) sets
|
|
15
|
+
* `output: 'raw-stream'` with a `rawStreamReason`. This check therefore fires only
|
|
16
|
+
* inside a `defineCommand({...})` whose `output` is NOT `'raw-stream'` — exactly
|
|
17
|
+
* where the host-owned-output contract applies. It is generic: it keys on the
|
|
18
|
+
* public command-authoring vocabulary every tool author uses, with no host path
|
|
19
|
+
* gating, so an adopter's own `fit` run enforces the same seam.
|
|
20
|
+
*/
|
|
21
|
+
import { defineCheck, isTestFile } from '@opensip-cli/fitness';
|
|
22
|
+
import { getSharedSourceFile } from '@opensip-cli/lang-typescript';
|
|
23
|
+
import * as ts from 'typescript';
|
|
24
|
+
/** The command-authoring factory whose specs the host mounts. */
|
|
25
|
+
const DEFINE_COMMAND = 'defineCommand';
|
|
26
|
+
/** The declared escape hatch: the handler owns its own output surface. */
|
|
27
|
+
const RAW_STREAM = 'raw-stream';
|
|
28
|
+
/** Output channels the host owns — a non-raw-stream handler must not write them itself. */
|
|
29
|
+
const FORBIDDEN_CALLS = [
|
|
30
|
+
{ chain: ['process', 'stdout', 'write'], label: 'process.stdout.write' },
|
|
31
|
+
{ chain: ['console', 'log'], label: 'console.log' },
|
|
32
|
+
{ chain: ['console', 'info'], label: 'console.info' },
|
|
33
|
+
{ chain: ['console', 'debug'], label: 'console.debug' },
|
|
34
|
+
{ chain: ['process', 'exit'], label: 'process.exit' },
|
|
35
|
+
];
|
|
36
|
+
function normalized(path) {
|
|
37
|
+
return path.replaceAll('\\', '/');
|
|
38
|
+
}
|
|
39
|
+
function propertyNameText(name) {
|
|
40
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name))
|
|
41
|
+
return name.text;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/** The string value of a property assignment whose initializer is a string literal. */
|
|
45
|
+
function stringPropValue(obj, key) {
|
|
46
|
+
for (const prop of obj.properties) {
|
|
47
|
+
if (!ts.isPropertyAssignment(prop))
|
|
48
|
+
continue;
|
|
49
|
+
if (propertyNameText(prop.name) !== key)
|
|
50
|
+
continue;
|
|
51
|
+
if (ts.isStringLiteral(prop.initializer))
|
|
52
|
+
return prop.initializer.text;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/** The handler initializer (arrow / function) of a `defineCommand` object literal. */
|
|
57
|
+
function handlerBody(obj) {
|
|
58
|
+
for (const prop of obj.properties) {
|
|
59
|
+
if (ts.isPropertyAssignment(prop) && propertyNameText(prop.name) === 'handler') {
|
|
60
|
+
const init = prop.initializer;
|
|
61
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init))
|
|
62
|
+
return init.body;
|
|
63
|
+
}
|
|
64
|
+
if (ts.isMethodDeclaration(prop) &&
|
|
65
|
+
propertyNameText(prop.name) === 'handler' &&
|
|
66
|
+
prop.body !== undefined) {
|
|
67
|
+
return prop.body;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
/** The dotted property chain of a call's callee (`process.stdout.write` → ['process','stdout','write']). */
|
|
73
|
+
function calleeChain(expr) {
|
|
74
|
+
const parts = [];
|
|
75
|
+
let node = expr;
|
|
76
|
+
while (ts.isPropertyAccessExpression(node)) {
|
|
77
|
+
parts.unshift(node.name.text);
|
|
78
|
+
node = node.expression;
|
|
79
|
+
}
|
|
80
|
+
if (!ts.isIdentifier(node))
|
|
81
|
+
return undefined;
|
|
82
|
+
parts.unshift(node.text);
|
|
83
|
+
return parts;
|
|
84
|
+
}
|
|
85
|
+
function chainEquals(actual, expected) {
|
|
86
|
+
if (actual.length !== expected.length)
|
|
87
|
+
return false;
|
|
88
|
+
return actual.every((part, i) => part === expected[i]);
|
|
89
|
+
}
|
|
90
|
+
/** Find each forbidden host-owned-output call within a handler body subtree. */
|
|
91
|
+
function findForbiddenCalls(body, sourceFile, filePath) {
|
|
92
|
+
const violations = [];
|
|
93
|
+
const visit = (node) => {
|
|
94
|
+
if (ts.isCallExpression(node)) {
|
|
95
|
+
const chain = calleeChain(node.expression);
|
|
96
|
+
if (chain) {
|
|
97
|
+
for (const { chain: forbidden, label } of FORBIDDEN_CALLS) {
|
|
98
|
+
if (chainEquals(chain, forbidden)) {
|
|
99
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
100
|
+
violations.push({
|
|
101
|
+
filePath,
|
|
102
|
+
line,
|
|
103
|
+
message: `Command handler calls ${label} directly. A tool command lets the host own ` +
|
|
104
|
+
`rendering, --json, and the exit code — the handler returns its result and ` +
|
|
105
|
+
`routes output/exit through the host context (cli.render / cli.emitJson / ` +
|
|
106
|
+
`cli.emitEnvelope / cli.setExitCode). Direct stdout/exit bypasses that seam.`,
|
|
107
|
+
severity: 'error',
|
|
108
|
+
suggestion: `Return the command result (or a SignalEnvelope) and use the cli context to ` +
|
|
109
|
+
`emit and set the exit code. If this command genuinely owns its own output ` +
|
|
110
|
+
`surface (completion script, file export, subprocess IPC), declare ` +
|
|
111
|
+
`output: 'raw-stream' with a rawStreamReason instead.`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
ts.forEachChild(node, visit);
|
|
118
|
+
};
|
|
119
|
+
visit(body);
|
|
120
|
+
return violations;
|
|
121
|
+
}
|
|
122
|
+
/** Pure analysis over a parsed source file. Exported for unit tests. */
|
|
123
|
+
export function analyzeCommandHandlerHostOwnedOutput(content, filePath) {
|
|
124
|
+
const violations = [];
|
|
125
|
+
const sourceFile = getSharedSourceFile(filePath, content);
|
|
126
|
+
if (!sourceFile)
|
|
127
|
+
return violations;
|
|
128
|
+
const visit = (node) => {
|
|
129
|
+
if (ts.isCallExpression(node) &&
|
|
130
|
+
ts.isIdentifier(node.expression) &&
|
|
131
|
+
node.expression.text === DEFINE_COMMAND &&
|
|
132
|
+
node.arguments.length > 0) {
|
|
133
|
+
const arg = node.arguments[0];
|
|
134
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
135
|
+
const output = stringPropValue(arg, 'output');
|
|
136
|
+
// A raw-stream command DECLARES that it owns its own output surface.
|
|
137
|
+
if (output !== RAW_STREAM) {
|
|
138
|
+
const body = handlerBody(arg);
|
|
139
|
+
if (body)
|
|
140
|
+
violations.push(...findForbiddenCalls(body, sourceFile, filePath));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
ts.forEachChild(node, visit);
|
|
145
|
+
};
|
|
146
|
+
visit(sourceFile);
|
|
147
|
+
return violations;
|
|
148
|
+
}
|
|
149
|
+
export const commandHandlerHostOwnedOutput = defineCheck({
|
|
150
|
+
id: 'c1f4a2e7-6b83-4d59-9e0a-2f7c4b8d1a36',
|
|
151
|
+
slug: 'command-handler-host-owned-output',
|
|
152
|
+
description: 'A tool command handler must let the host own rendering and exit — no direct stdout/console/process.exit inside a non-raw-stream defineCommand handler',
|
|
153
|
+
scope: { languages: ['typescript'], concerns: ['backend'] },
|
|
154
|
+
tags: ['architecture'],
|
|
155
|
+
fileTypes: ['ts', 'tsx'],
|
|
156
|
+
analyze: (content, filePath) => {
|
|
157
|
+
if (isTestFile(filePath))
|
|
158
|
+
return [];
|
|
159
|
+
// Cheap pre-filter: only files that author a command can violate the rule.
|
|
160
|
+
if (!content.includes(DEFINE_COMMAND))
|
|
161
|
+
return [];
|
|
162
|
+
return analyzeCommandHandlerHostOwnedOutput(content, normalized(filePath));
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
//# sourceMappingURL=command-handler-host-owned-output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-handler-host-owned-output.js","sourceRoot":"","sources":["../../../src/checks/architecture/command-handler-host-owned-output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,WAAW,EAAE,UAAU,EAAuB,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,iEAAiE;AACjE,MAAM,cAAc,GAAG,eAAe,CAAC;AAEvC,0EAA0E;AAC1E,MAAM,UAAU,GAAG,YAAY,CAAC;AAEhC,2FAA2F;AAC3F,MAAM,eAAe,GAGf;IACJ,EAAE,KAAK,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE;IACxE,EAAE,KAAK,EAAE,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE;IACnD,EAAE,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE;IACrD,EAAE,KAAK,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE;IACvD,EAAE,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE;CACtD,CAAC;AAEF,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAqB;IAC7C,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC;IACxE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,uFAAuF;AACvF,SAAS,eAAe,CAAC,GAA+B,EAAE,GAAW;IACnE,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QAClC,IAAI,CAAC,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7C,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG;YAAE,SAAS;QAClD,IAAI,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC;YAAE,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;IACzE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,sFAAsF;AACtF,SAAS,WAAW,CAAC,GAA+B;IAClD,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QAClC,IAAI,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;YAC/E,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC;YAC9B,IAAI,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC,IAAI,CAAC;QAClF,CAAC;QACD,IACE,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC;YAC5B,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,SAAS;YACzC,IAAI,CAAC,IAAI,KAAK,SAAS,EACvB,CAAC;YACD,OAAO,IAAI,CAAC,IAAI,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,4GAA4G;AAC5G,SAAS,WAAW,CAAC,IAAmB;IACtC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,IAAI,GAAkB,IAAI,CAAC;IAC/B,OAAO,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IACD,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAC7C,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAAC,MAAyB,EAAE,QAA2B;IACzE,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACpD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,gFAAgF;AAChF,SAAS,kBAAkB,CACzB,IAAa,EACb,UAAyB,EACzB,QAAgB;IAEhB,MAAM,UAAU,GAAqB,EAAE,CAAC;IACxC,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QACpC,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC3C,IAAI,KAAK,EAAE,CAAC;gBACV,KAAK,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,eAAe,EAAE,CAAC;oBAC1D,IAAI,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;wBAClC,MAAM,IAAI,GACR,UAAU,CAAC,6BAA6B,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;wBAC/E,UAAU,CAAC,IAAI,CAAC;4BACd,QAAQ;4BACR,IAAI;4BACJ,OAAO,EACL,yBAAyB,KAAK,8CAA8C;gCAC5E,4EAA4E;gCAC5E,2EAA2E;gCAC3E,6EAA6E;4BAC/E,QAAQ,EAAE,OAAO;4BACjB,UAAU,EACR,6EAA6E;gCAC7E,4EAA4E;gCAC5E,oEAAoE;gCACpE,sDAAsD;yBACzD,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QACD,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC/B,CAAC,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,CAAC;IACZ,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,oCAAoC,CAClD,OAAe,EACf,QAAgB;IAEhB,MAAM,UAAU,GAAqB,EAAE,CAAC;IACxC,MAAM,UAAU,GAAG,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC1D,IAAI,CAAC,UAAU;QAAE,OAAO,UAAU,CAAC;IAEnC,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QACpC,IACE,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC;YACzB,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC;YAChC,IAAI,CAAC,UAAU,CAAC,IAAI,KAAK,cAAc;YACvC,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EACzB,CAAC;YACD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,EAAE,CAAC,yBAAyB,CAAC,GAAG,CAAC,EAAE,CAAC;gBACtC,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;gBAC9C,qEAAqE;gBACrE,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;oBAC1B,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;oBAC9B,IAAI,IAAI;wBAAE,UAAU,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC;gBAC/E,CAAC;YACH,CAAC;QACH,CAAC;QACD,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC/B,CAAC,CAAC;IACF,KAAK,CAAC,UAAU,CAAC,CAAC;IAClB,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,MAAM,6BAA6B,GAAG,WAAW,CAAC;IACvD,EAAE,EAAE,sCAAsC;IAC1C,IAAI,EAAE,mCAAmC;IACzC,WAAW,EACT,uJAAuJ;IACzJ,KAAK,EAAE,EAAE,SAAS,EAAE,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAC,SAAS,CAAC,EAAE;IAC3D,IAAI,EAAE,CAAC,cAAc,CAAC;IACtB,SAAS,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC;IACxB,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;QAC7B,IAAI,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QACpC,2EAA2E;QAC3E,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;YAAE,OAAO,EAAE,CAAC;QACjD,OAAO,oCAAoC,CAAC,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7E,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Mechanize the ADR-0054 M4-G CAPSTONE invariant: no
|
|
3
|
+
* `importToolRuntime(...)` for external-provenance tools in the HOST process.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* boundary.
|
|
5
|
+
* After the capstone, external tool runtime code NEVER loads in the host. The
|
|
6
|
+
* host registers a manifest-derived synthetic Tool (command shells from the
|
|
7
|
+
* static manifest) and mounts its commands without importing the runtime; the
|
|
8
|
+
* forked dispatch WORKER imports the untrusted runtime when a command actually
|
|
9
|
+
* dispatches. This is no longer a transition guardrail — it is the enforced
|
|
10
|
+
* boundary. The check asserts three things across the CLI host package:
|
|
11
|
+
*
|
|
12
|
+
* 1. `importToolRuntime` may be called only from the admission/discovery
|
|
13
|
+
* boundary or the worker-owned dispatch plane (the allowlisted files).
|
|
14
|
+
* Any other host-process call site is a violation.
|
|
15
|
+
* 2. A HOST import must pass a BUNDLED policy — `hostRuntimeImportPolicyFor(...)`
|
|
16
|
+
* (type-narrowed to `'bundled'`) or a literal `{ source: 'bundled' }`. A
|
|
17
|
+
* non-bundled host policy is a violation (the type already forbids it; this
|
|
18
|
+
* catches a hand-rolled literal that bypasses the constructor).
|
|
19
|
+
* 3. `workerRuntimeImportPolicyFor(...)` — the policy that authorizes loading an
|
|
20
|
+
* EXTERNAL runtime — is permitted only on the worker-owned plane (the
|
|
21
|
+
* discovery leg gated behind `isHostRuntimeImportForbidden`, the worker entry,
|
|
22
|
+
* and the admission module that defines it). Using it elsewhere would import
|
|
23
|
+
* an external runtime in the host.
|
|
24
|
+
*
|
|
25
|
+
* The `adr0054Transition` exception is gone: the worker is now the only path for
|
|
26
|
+
* external runtime import, and the host import policy is bundled-only.
|
|
10
27
|
*/
|
|
11
28
|
import { type CheckViolation } from '@opensip-cli/fitness';
|
|
12
29
|
/** Pure analysis over a parsed source file. Exported for unit tests. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-tool-runtime-import-boundary.d.ts","sourceRoot":"","sources":["../../../src/checks/architecture/host-tool-runtime-import-boundary.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"host-tool-runtime-import-boundary.d.ts","sourceRoot":"","sources":["../../../src/checks/architecture/host-tool-runtime-import-boundary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA2HpF,wEAAwE;AACxE,wBAAgB,oCAAoC,CAClD,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,cAAc,EAAE,CA2DlB;AAED,eAAO,MAAM,6BAA6B,sCAYxC,CAAC"}
|