@ontrails/core 1.0.0-beta.30 → 1.0.0-beta.39
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/CHANGELOG.md +63 -0
- package/package.json +1 -1
- package/src/context.ts +18 -4
- package/src/derive.ts +30 -3
- package/src/diagnostics.ts +21 -0
- package/src/draft.ts +18 -10
- package/src/glob.ts +81 -0
- package/src/index.ts +79 -4
- package/src/path-scope.ts +66 -0
- package/src/path-security.ts +10 -2
- package/src/resource-config.ts +16 -4
- package/src/runtime-builtins.ts +69 -0
- package/src/sha256.ts +136 -0
- package/src/signal-diagnostics.ts +4 -4
- package/src/surface-filter.ts +3 -78
- package/src/surface-overlay.ts +694 -0
- package/src/trail-id-glob.ts +15 -0
- package/src/trail.ts +78 -0
- package/src/trails-db.ts +28 -22
- package/src/validate-established-topo.ts +2 -2
- package/src/validate-topo.ts +215 -64
- package/src/webhook.ts +194 -11
- package/src/workspace.ts +182 -15
package/src/resource-config.ts
CHANGED
|
@@ -585,7 +585,7 @@ const withResolvedResources = (
|
|
|
585
585
|
/**
|
|
586
586
|
* Resolved trail context plus lease release for resources used by the run.
|
|
587
587
|
*/
|
|
588
|
-
interface ResolvedResourceScope {
|
|
588
|
+
export interface ResolvedResourceScope {
|
|
589
589
|
readonly ctx: TrailContext;
|
|
590
590
|
release(): void;
|
|
591
591
|
}
|
|
@@ -593,14 +593,26 @@ interface ResolvedResourceScope {
|
|
|
593
593
|
const releaseNoResources = (): undefined => undefined;
|
|
594
594
|
|
|
595
595
|
/**
|
|
596
|
-
* Resolve all declared resources for a
|
|
596
|
+
* Resolve all declared resources for a resource-bearing declaration.
|
|
597
597
|
*
|
|
598
598
|
* Validates per-resource config, checks overrides and caches, and creates
|
|
599
599
|
* new instances as needed. Returns an enriched context with all resource
|
|
600
|
-
* instances injected into extensions.
|
|
600
|
+
* instances injected into extensions. Callers own the returned `release`.
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* ```ts
|
|
604
|
+
* const scope = await createResources(
|
|
605
|
+
* { resources: [secretsStore] },
|
|
606
|
+
* createTrailContext()
|
|
607
|
+
* );
|
|
608
|
+
* if (scope.isOk()) {
|
|
609
|
+
* const secrets = secretsStore.from(scope.value.ctx);
|
|
610
|
+
* scope.value.release();
|
|
611
|
+
* }
|
|
612
|
+
* ```
|
|
601
613
|
*/
|
|
602
614
|
export const createResources = async (
|
|
603
|
-
trail: AnyTrail,
|
|
615
|
+
trail: Pick<AnyTrail, 'resources'>,
|
|
604
616
|
ctx: TrailContext,
|
|
605
617
|
overrides?: ResourceOverrideMap,
|
|
606
618
|
configValues?: ConfigValues
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy runtime-builtin loading for the execution-portable core barrel.
|
|
3
|
+
*
|
|
4
|
+
* Core tooling modules that need Bun or Node capabilities (`bun:sqlite`,
|
|
5
|
+
* `node:fs`, `node:os`, `node:path`) must not import them eagerly: the
|
|
6
|
+
* core barrel sits on the execution path of every surface, and edge
|
|
7
|
+
* runtimes such as workerd refuse module graphs that import `bun:`
|
|
8
|
+
* builtins and gate `node:` builtins behind compatibility flags
|
|
9
|
+
* (TRL-1198). Loading through `process.getBuiltinModule` keeps the
|
|
10
|
+
* specifier out of bundler module graphs entirely and defers the
|
|
11
|
+
* capability requirement to first use, so runtimes that never call a
|
|
12
|
+
* tooling helper never pay for it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type * as BunSqlite from 'bun:sqlite';
|
|
16
|
+
import type * as NodeFs from 'node:fs';
|
|
17
|
+
import type * as NodeOs from 'node:os';
|
|
18
|
+
import type * as NodePath from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { InternalError } from './errors.js';
|
|
21
|
+
|
|
22
|
+
interface BuiltinModules {
|
|
23
|
+
readonly 'bun:sqlite': typeof BunSqlite;
|
|
24
|
+
readonly 'node:fs': typeof NodeFs;
|
|
25
|
+
readonly 'node:os': typeof NodeOs;
|
|
26
|
+
readonly 'node:path': typeof NodePath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const loadedBuiltins = new Map<keyof BuiltinModules, unknown>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load a Bun/Node builtin module at first use.
|
|
33
|
+
*
|
|
34
|
+
* @throws {InternalError} When the runtime does not expose the builtin
|
|
35
|
+
* (for example workerd without `nodejs_compat`). Trail execution never
|
|
36
|
+
* reaches this loader; only tooling helpers (trails-db, workspace
|
|
37
|
+
* discovery) do.
|
|
38
|
+
*/
|
|
39
|
+
export const loadRuntimeBuiltin = <TName extends keyof BuiltinModules>(
|
|
40
|
+
name: TName
|
|
41
|
+
): BuiltinModules[TName] => {
|
|
42
|
+
const cached = loadedBuiltins.get(name);
|
|
43
|
+
if (cached !== undefined) {
|
|
44
|
+
return cached as BuiltinModules[TName];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const proc = (
|
|
48
|
+
globalThis as {
|
|
49
|
+
readonly process?: {
|
|
50
|
+
readonly getBuiltinModule?: (id: string) => unknown;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
).process;
|
|
54
|
+
if (typeof proc?.getBuiltinModule !== 'function') {
|
|
55
|
+
throw new InternalError(
|
|
56
|
+
`Runtime builtin "${name}" is unavailable: this runtime does not expose process.getBuiltinModule. Trails tooling helpers need a Bun or Node runtime; the trail execution path never loads them.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const loaded = proc.getBuiltinModule(name);
|
|
61
|
+
if (loaded === undefined || loaded === null) {
|
|
62
|
+
throw new InternalError(
|
|
63
|
+
`Runtime builtin "${name}" is unavailable on this runtime.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
loadedBuiltins.set(name, loaded);
|
|
68
|
+
return loaded as BuiltinModules[TName];
|
|
69
|
+
};
|
package/src/sha256.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure synchronous SHA-256 for runtime-portable fingerprints.
|
|
3
|
+
*
|
|
4
|
+
* `node:crypto`'s `createHash` is unavailable on edge runtimes without
|
|
5
|
+
* compatibility flags, and Web Crypto's `subtle.digest` is asynchronous,
|
|
6
|
+
* which does not fit the synchronous summary paths that need a digest
|
|
7
|
+
* (signal payload summaries, per-project store keys). This implementation
|
|
8
|
+
* follows FIPS 180-4 and produces output identical to
|
|
9
|
+
* `createHash('sha256').update(text).digest('hex')`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/* oxlint-disable no-bitwise, unicorn/prefer-math-trunc, unicorn/number-literal-case -- SHA-256 is defined over 32-bit words: rotations, xor, and `>>> 0` unsigned wrapping are the specified operations, and `Math.trunc()` is not equivalent to `>>> 0` (it neither wraps to 32 bits nor coerces to unsigned). number-literal-case is disabled because oxfmt normalizes hex digits to lowercase, which the rule rejects — the formatter wins. */
|
|
13
|
+
|
|
14
|
+
// FIPS 180-4 §4.2.2: first 32 bits of the fractional parts of the cube
|
|
15
|
+
// roots of the first 64 primes.
|
|
16
|
+
const K = Uint32Array.from([
|
|
17
|
+
0x42_8a_2f_98, 0x71_37_44_91, 0xb5_c0_fb_cf, 0xe9_b5_db_a5, 0x39_56_c2_5b,
|
|
18
|
+
0x59_f1_11_f1, 0x92_3f_82_a4, 0xab_1c_5e_d5, 0xd8_07_aa_98, 0x12_83_5b_01,
|
|
19
|
+
0x24_31_85_be, 0x55_0c_7d_c3, 0x72_be_5d_74, 0x80_de_b1_fe, 0x9b_dc_06_a7,
|
|
20
|
+
0xc1_9b_f1_74, 0xe4_9b_69_c1, 0xef_be_47_86, 0x0f_c1_9d_c6, 0x24_0c_a1_cc,
|
|
21
|
+
0x2d_e9_2c_6f, 0x4a_74_84_aa, 0x5c_b0_a9_dc, 0x76_f9_88_da, 0x98_3e_51_52,
|
|
22
|
+
0xa8_31_c6_6d, 0xb0_03_27_c8, 0xbf_59_7f_c7, 0xc6_e0_0b_f3, 0xd5_a7_91_47,
|
|
23
|
+
0x06_ca_63_51, 0x14_29_29_67, 0x27_b7_0a_85, 0x2e_1b_21_38, 0x4d_2c_6d_fc,
|
|
24
|
+
0x53_38_0d_13, 0x65_0a_73_54, 0x76_6a_0a_bb, 0x81_c2_c9_2e, 0x92_72_2c_85,
|
|
25
|
+
0xa2_bf_e8_a1, 0xa8_1a_66_4b, 0xc2_4b_8b_70, 0xc7_6c_51_a3, 0xd1_92_e8_19,
|
|
26
|
+
0xd6_99_06_24, 0xf4_0e_35_85, 0x10_6a_a0_70, 0x19_a4_c1_16, 0x1e_37_6c_08,
|
|
27
|
+
0x27_48_77_4c, 0x34_b0_bc_b5, 0x39_1c_0c_b3, 0x4e_d8_aa_4a, 0x5b_9c_ca_4f,
|
|
28
|
+
0x68_2e_6f_f3, 0x74_8f_82_ee, 0x78_a5_63_6f, 0x84_c8_78_14, 0x8c_c7_02_08,
|
|
29
|
+
0x90_be_ff_fa, 0xa4_50_6c_eb, 0xbe_f9_a3_f7, 0xc6_71_78_f2,
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const rotr = (value: number, bits: number): number =>
|
|
33
|
+
(value >>> bits) | (value << (32 - bits));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Total array accessor: every read in the compression loop is provably in
|
|
37
|
+
* range, so the fallback never fires — it only satisfies
|
|
38
|
+
* `noUncheckedIndexedAccess` without an assertion.
|
|
39
|
+
*/
|
|
40
|
+
const wordAt = (words: Uint32Array, index: number): number => words[index] ?? 0;
|
|
41
|
+
|
|
42
|
+
const padMessage = (bytes: Uint8Array): Uint8Array => {
|
|
43
|
+
const bitLength = bytes.length * 8;
|
|
44
|
+
// Message + 0x80 marker, padded to 56 mod 64, then a 64-bit big-endian
|
|
45
|
+
// bit length. Message sizes here are far below 2^32 bits, so the high
|
|
46
|
+
// word of the length is derived from the float division.
|
|
47
|
+
const paddedLength = (Math.floor((bytes.length + 8) / 64) + 1) * 64;
|
|
48
|
+
const padded = new Uint8Array(paddedLength);
|
|
49
|
+
padded.set(bytes);
|
|
50
|
+
padded[bytes.length] = 0x80;
|
|
51
|
+
const view = new DataView(padded.buffer);
|
|
52
|
+
view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x1_00_00_00_00));
|
|
53
|
+
view.setUint32(paddedLength - 4, bitLength >>> 0);
|
|
54
|
+
return padded;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const fillSchedule = (w: Uint32Array, view: DataView, offset: number): void => {
|
|
58
|
+
for (let i = 0; i < 16; i += 1) {
|
|
59
|
+
w[i] = view.getUint32(offset + i * 4);
|
|
60
|
+
}
|
|
61
|
+
for (let i = 16; i < 64; i += 1) {
|
|
62
|
+
const w15 = wordAt(w, i - 15);
|
|
63
|
+
const w2 = wordAt(w, i - 2);
|
|
64
|
+
const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
|
|
65
|
+
const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
|
|
66
|
+
w[i] = (wordAt(w, i - 16) + s0 + wordAt(w, i - 7) + s1) >>> 0;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// oxlint-disable-next-line max-statements -- the FIPS 180-4 compression loop reads more clearly as one block than split across helpers
|
|
71
|
+
const digestHex = (padded: Uint8Array): string => {
|
|
72
|
+
// FIPS 180-4 §5.3.3 initial hash value.
|
|
73
|
+
let h0 = 0x6a_09_e6_67;
|
|
74
|
+
let h1 = 0xbb_67_ae_85;
|
|
75
|
+
let h2 = 0x3c_6e_f3_72;
|
|
76
|
+
let h3 = 0xa5_4f_f5_3a;
|
|
77
|
+
let h4 = 0x51_0e_52_7f;
|
|
78
|
+
let h5 = 0x9b_05_68_8c;
|
|
79
|
+
let h6 = 0x1f_83_d9_ab;
|
|
80
|
+
let h7 = 0x5b_e0_cd_19;
|
|
81
|
+
|
|
82
|
+
const view = new DataView(padded.buffer);
|
|
83
|
+
const w = new Uint32Array(64);
|
|
84
|
+
|
|
85
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
86
|
+
fillSchedule(w, view, offset);
|
|
87
|
+
|
|
88
|
+
let a = h0;
|
|
89
|
+
let b = h1;
|
|
90
|
+
let c = h2;
|
|
91
|
+
let d = h3;
|
|
92
|
+
let e = h4;
|
|
93
|
+
let f = h5;
|
|
94
|
+
let g = h6;
|
|
95
|
+
let h = h7;
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < 64; i += 1) {
|
|
98
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
99
|
+
const ch = (e & f) ^ (~e & g);
|
|
100
|
+
const temp1 = (h + s1 + ch + wordAt(K, i) + wordAt(w, i)) >>> 0;
|
|
101
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
102
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
103
|
+
const temp2 = (s0 + maj) >>> 0;
|
|
104
|
+
|
|
105
|
+
h = g;
|
|
106
|
+
g = f;
|
|
107
|
+
f = e;
|
|
108
|
+
e = (d + temp1) >>> 0;
|
|
109
|
+
d = c;
|
|
110
|
+
c = b;
|
|
111
|
+
b = a;
|
|
112
|
+
a = (temp1 + temp2) >>> 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
h0 = (h0 + a) >>> 0;
|
|
116
|
+
h1 = (h1 + b) >>> 0;
|
|
117
|
+
h2 = (h2 + c) >>> 0;
|
|
118
|
+
h3 = (h3 + d) >>> 0;
|
|
119
|
+
h4 = (h4 + e) >>> 0;
|
|
120
|
+
h5 = (h5 + f) >>> 0;
|
|
121
|
+
h6 = (h6 + g) >>> 0;
|
|
122
|
+
h7 = (h7 + h) >>> 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [h0, h1, h2, h3, h4, h5, h6, h7]
|
|
126
|
+
.map((word) => word.toString(16).padStart(8, '0'))
|
|
127
|
+
.join('');
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* SHA-256 of the UTF-8 encoding of `text`, as lowercase hex.
|
|
132
|
+
*
|
|
133
|
+
* Output-identical to `createHash('sha256').update(text).digest('hex')`.
|
|
134
|
+
*/
|
|
135
|
+
export const sha256Hex = (text: string): string =>
|
|
136
|
+
digestHex(padMessage(new TextEncoder().encode(text)));
|
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
* observable without changing the best-effort producer API.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { createHash } from 'node:crypto';
|
|
9
|
-
|
|
10
8
|
import type { z } from 'zod';
|
|
11
9
|
|
|
12
10
|
import type { ActivationProvenance } from './activation-provenance.js';
|
|
11
|
+
import { sha256Hex } from './sha256.js';
|
|
13
12
|
import type { Logger } from './types.js';
|
|
14
13
|
|
|
15
14
|
export const SIGNAL_DIAGNOSTICS_SINK_KEY =
|
|
@@ -349,8 +348,9 @@ const stableJson = (value: unknown): string => {
|
|
|
349
348
|
}
|
|
350
349
|
};
|
|
351
350
|
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
// Pure sha256 keeps payload summarization on the execution path portable:
|
|
352
|
+
// node:crypto is unavailable on edge runtimes without compat flags.
|
|
353
|
+
const hashText = (text: string): string => sha256Hex(text);
|
|
354
354
|
|
|
355
355
|
export const summarizeSignalPayload = (
|
|
356
356
|
payload: unknown
|
package/src/surface-filter.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Intent, Trail } from './trail.js';
|
|
2
|
+
import { matchesAnyTrailIdGlob, matchesTrailIdGlob } from './trail-id-glob.js';
|
|
2
3
|
import type { SurfaceSelectionOptions } from './surface-derivation.js';
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
@@ -7,84 +8,10 @@ import type { SurfaceSelectionOptions } from './surface-derivation.js';
|
|
|
7
8
|
|
|
8
9
|
export type SurfaceFilterOptions = SurfaceSelectionOptions;
|
|
9
10
|
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Glob matching
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
const trailIdSegments = (trailId: string): readonly string[] =>
|
|
15
|
-
trailId.split('.');
|
|
16
|
-
|
|
17
|
-
const patternSegments = (pattern: string): readonly string[] =>
|
|
18
|
-
pattern.split('.');
|
|
19
|
-
|
|
20
|
-
type SegmentMatcher = (
|
|
21
|
-
trail: readonly string[],
|
|
22
|
-
pattern: readonly string[],
|
|
23
|
-
trailIndex: number,
|
|
24
|
-
patternIndex: number
|
|
25
|
-
) => boolean;
|
|
26
|
-
|
|
27
|
-
const matchesDoubleStar = (
|
|
28
|
-
trail: readonly string[],
|
|
29
|
-
pattern: readonly string[],
|
|
30
|
-
trailIndex: number,
|
|
31
|
-
patternIndex: number,
|
|
32
|
-
matchSegments: SegmentMatcher
|
|
33
|
-
): boolean =>
|
|
34
|
-
patternIndex === pattern.length - 1 ||
|
|
35
|
-
Array.from(
|
|
36
|
-
{ length: trail.length - trailIndex + 1 },
|
|
37
|
-
(_, offset) => trailIndex + offset
|
|
38
|
-
).some((index) => matchSegments(trail, pattern, index, patternIndex + 1));
|
|
39
|
-
|
|
40
|
-
const matchesSingleSegment = (
|
|
41
|
-
trail: readonly string[],
|
|
42
|
-
pattern: readonly string[],
|
|
43
|
-
trailIndex: number,
|
|
44
|
-
patternIndex: number,
|
|
45
|
-
current: string,
|
|
46
|
-
matchSegments: SegmentMatcher
|
|
47
|
-
): boolean =>
|
|
48
|
-
trailIndex < trail.length &&
|
|
49
|
-
(current === '*' || current === trail[trailIndex]) &&
|
|
50
|
-
matchSegments(trail, pattern, trailIndex + 1, patternIndex + 1);
|
|
51
|
-
|
|
52
|
-
const matchesFrom = function matchesFrom(
|
|
53
|
-
trail: readonly string[],
|
|
54
|
-
pattern: readonly string[],
|
|
55
|
-
trailIndex: number,
|
|
56
|
-
patternIndex: number
|
|
57
|
-
): boolean {
|
|
58
|
-
if (patternIndex >= pattern.length) {
|
|
59
|
-
return trailIndex >= trail.length;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const current = pattern[patternIndex];
|
|
63
|
-
if (current === undefined) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return current === '**'
|
|
68
|
-
? matchesDoubleStar(trail, pattern, trailIndex, patternIndex, matchesFrom)
|
|
69
|
-
: matchesSingleSegment(
|
|
70
|
-
trail,
|
|
71
|
-
pattern,
|
|
72
|
-
trailIndex,
|
|
73
|
-
patternIndex,
|
|
74
|
-
current,
|
|
75
|
-
matchesFrom
|
|
76
|
-
);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
11
|
export const matchesTrailPattern = (
|
|
80
12
|
trailId: string,
|
|
81
13
|
pattern: string
|
|
82
|
-
): boolean =>
|
|
83
|
-
if (pattern === trailId) {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
return matchesFrom(trailIdSegments(trailId), patternSegments(pattern), 0, 0);
|
|
87
|
-
};
|
|
14
|
+
): boolean => matchesTrailIdGlob(trailId, pattern);
|
|
88
15
|
|
|
89
16
|
// ---------------------------------------------------------------------------
|
|
90
17
|
// Surface filtering
|
|
@@ -93,9 +20,7 @@ export const matchesTrailPattern = (
|
|
|
93
20
|
const matchesAnyPattern = (
|
|
94
21
|
trailId: string,
|
|
95
22
|
patterns: readonly string[] | undefined
|
|
96
|
-
): boolean =>
|
|
97
|
-
patterns !== undefined &&
|
|
98
|
-
patterns.some((pattern) => matchesTrailPattern(trailId, pattern));
|
|
23
|
+
): boolean => matchesAnyTrailIdGlob(trailId, patterns);
|
|
99
24
|
|
|
100
25
|
const isExplicitInternalInclude = (
|
|
101
26
|
trailId: string,
|