@tiphys/kernel 0.1.0 → 0.2.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.
Files changed (94) hide show
  1. package/AGENTS.md +56 -4
  2. package/assurance-modes.yaml +23 -2
  3. package/dist/bin/tiphys.js +86 -8
  4. package/dist/src/adapters/load.d.ts +202 -0
  5. package/dist/src/adapters/load.js +440 -0
  6. package/dist/src/brief.js +27 -20
  7. package/dist/src/checks.d.ts +720 -9
  8. package/dist/src/checks.js +1874 -163
  9. package/dist/src/cli.js +11 -0
  10. package/dist/src/commands/brief.js +27 -4
  11. package/dist/src/commands/cutover.d.ts +35 -0
  12. package/dist/src/commands/cutover.js +448 -0
  13. package/dist/src/commands/doctor.d.ts +229 -0
  14. package/dist/src/commands/doctor.js +968 -27
  15. package/dist/src/commands/init.d.ts +3 -3
  16. package/dist/src/commands/init.js +57 -8
  17. package/dist/src/commands/lock.d.ts +33 -0
  18. package/dist/src/commands/lock.js +117 -6
  19. package/dist/src/commands/next.d.ts +130 -0
  20. package/dist/src/commands/next.js +597 -0
  21. package/dist/src/commands/pool.js +12 -1
  22. package/dist/src/commands/resume.d.ts +1 -0
  23. package/dist/src/commands/resume.js +88 -0
  24. package/dist/src/commands/spawn.js +51 -2
  25. package/dist/src/commands/status.d.ts +6 -4
  26. package/dist/src/commands/status.js +6 -4
  27. package/dist/src/commands/sync.d.ts +47 -0
  28. package/dist/src/commands/sync.js +341 -0
  29. package/dist/src/commands/teardown.js +10 -2
  30. package/dist/src/commands/validate.js +70 -0
  31. package/dist/src/cutover.d.ts +584 -0
  32. package/dist/src/cutover.js +1444 -0
  33. package/dist/src/exclusion.d.ts +389 -0
  34. package/dist/src/exclusion.js +843 -0
  35. package/dist/src/exec/env.d.ts +152 -2
  36. package/dist/src/exec/env.js +146 -2
  37. package/dist/src/fleet.d.ts +172 -0
  38. package/dist/src/fleet.js +219 -1
  39. package/dist/src/gates/citations.js +7 -1
  40. package/dist/src/gates/coverage.d.ts +113 -22
  41. package/dist/src/gates/coverage.js +166 -31
  42. package/dist/src/gates/credentials.d.ts +159 -0
  43. package/dist/src/gates/credentials.js +221 -2
  44. package/dist/src/gates/gate-classes.d.ts +56 -0
  45. package/dist/src/gates/gate-classes.js +633 -0
  46. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  47. package/dist/src/gates/merge-preconditions.js +932 -0
  48. package/dist/src/gates/red-witness.js +105 -13
  49. package/dist/src/gates/run.d.ts +49 -1
  50. package/dist/src/gates/run.js +83 -5
  51. package/dist/src/gates/schemas/phase-declaration.schema.json +45 -0
  52. package/dist/src/gates/suite.js +48 -7
  53. package/dist/src/hooks.d.ts +55 -3
  54. package/dist/src/hooks.js +69 -6
  55. package/dist/src/index.d.ts +31 -0
  56. package/dist/src/index.js +30 -0
  57. package/dist/src/lock.d.ts +82 -4
  58. package/dist/src/lock.js +314 -22
  59. package/dist/src/model-resolution.d.ts +159 -0
  60. package/dist/src/model-resolution.js +307 -0
  61. package/dist/src/path-identity.d.ts +32 -0
  62. package/dist/src/path-identity.js +38 -0
  63. package/dist/src/pool.d.ts +197 -1
  64. package/dist/src/pool.js +289 -22
  65. package/dist/src/roles.d.ts +31 -0
  66. package/dist/src/roles.js +42 -0
  67. package/dist/src/spawn.d.ts +307 -2
  68. package/dist/src/spawn.js +690 -19
  69. package/dist/src/status.d.ts +27 -2
  70. package/dist/src/status.js +34 -5
  71. package/dist/src/task.d.ts +295 -55
  72. package/dist/src/task.js +125 -123
  73. package/dist/src/teardown.d.ts +7 -0
  74. package/dist/src/teardown.js +120 -12
  75. package/dist/src/validate.d.ts +44 -11
  76. package/dist/src/validate.js +44 -34
  77. package/dist/src/watcher.js +1 -11
  78. package/dist/src/witness/run.d.ts +32 -7
  79. package/dist/src/witness/run.js +76 -30
  80. package/dist/src/witness/spec.d.ts +168 -0
  81. package/dist/src/witness/spec.js +240 -18
  82. package/dist/tsconfig.src.tsbuildinfo +1 -1
  83. package/gate-registry.yaml +136 -0
  84. package/gates.manifest.json +63 -1
  85. package/package.json +18 -3
  86. package/roles/implementer.md +3 -0
  87. package/schemas/README.md +1 -0
  88. package/schemas/assurance-modes.schema.json +1 -1
  89. package/schemas/charter.schema.json +19 -0
  90. package/schemas/cutover-state.schema.json +64 -0
  91. package/schemas/executor-record.schema.json +36 -0
  92. package/schemas/model-resolution.schema.json +362 -0
  93. package/schemas/verdict.schema.json +9 -3
  94. package/schemas/write-bypass.schema.json +69 -0
package/dist/src/hooks.js CHANGED
@@ -29,9 +29,71 @@ export function turnEndHookPath(fleet, taskId) {
29
29
  }
30
30
  /**
31
31
  * Generate the hook script. The turn-end path is baked in as a literal,
32
- * so the hook needs no fleet resolution and no environment at all.
32
+ * so the hook needs no fleet resolution.
33
+ *
34
+ * THE CHILD-OBSERVED POINTER RECORD (CR-B-001, the half that closes the hole
35
+ * rather than the half that stops mis-asserting it).
36
+ *
37
+ * `observeNames`, when given, is baked in as a literal array and the hook
38
+ * writes an `env` object holding what each of those names ACTUALLY IS in the
39
+ * environment the hook was launched with, or `null` where the name is unset.
40
+ * The kernel compares those against the harness-owned paths it handed over.
41
+ *
42
+ * WHY THIS IS STRONGER THAN AN ADAPTER'S REPORT AND WHY IT IS NOT PROOF.
43
+ * This script is written BY THE KERNEL and, for every adapter that honours
44
+ * the documented contract, runs in the SAME environment as the payload (the
45
+ * built-in adapter spreads the same `request.env` into both spawnSync calls,
46
+ * and M2R-004 edit 4 is the record of why a second unscrubbed launch is
47
+ * itself the leak). Against an adapter that does not invoke this script at
48
+ * all it proves nothing, and the record no longer says otherwise.
49
+ *
50
+ * THE COST SENTENCE THAT STOOD HERE IS WITHDRAWN, BECAUSE IT WAS REFUTED BY
51
+ * MEASUREMENT (CR-F-CRED-001, MEDIUM).
52
+ *
53
+ * It read: an adapter that quietly reverted `HOME` for the payload "has to
54
+ * revert it for the payload and NOT for the hook, which means launching two
55
+ * children with two different environments and is a substantially different
56
+ * act from passing a mutated copy once". Two children is one way to do it and
57
+ * it is not the cheap way. The cheap way is ONE child with the mutated
58
+ * environment plus a single `writeFileSync` of the turn-end path, which is
59
+ * STRICTLY LESS work than the honest path, because the honest path also
60
+ * spawns the hook. The turn-end path is handed to the adapter beside
61
+ * `hookPath`, and this generated script names it as a literal, so an adapter
62
+ * that never runs it can still produce a byte-identical record.
63
+ *
64
+ * WHY A NONCE DOES NOT CLOSE THIS, stated because it is the obvious repair and
65
+ * it was considered and refused rather than overlooked. Baking a per-task
66
+ * secret into this script and requiring it in the record moves the forgery
67
+ * from "know the path" to "read the file", and the adapter is HANDED the path
68
+ * of this file: it runs at the same uid, on the same filesystem, in a
69
+ * directory it must be able to read to invoke the hook at all. A guard whose
70
+ * condition the adversary can satisfy by reading one file is green and
71
+ * worthless, which is this repository's own recorded shape (T-008's
72
+ * postscript, the red-witness rule one level up). No artifact this script can
73
+ * write is unforgeable by a party that can read this script.
74
+ *
75
+ * So the repair is on the RECORD rather than on the check: the value is
76
+ * `turn-end-record`, it names the artifact the values were read from, and
77
+ * `CredentialHandoverRecord` (src/task.ts) says the artifact is
78
+ * adapter-reachable. A dishonest adapter is still not caught here, and that
79
+ * residue is declared at delivery/work-history/m4-p8.md item 3; what changed
80
+ * is that `meta.json` no longer positively asserts a child-side observation
81
+ * that no child made.
82
+ *
83
+ * The hook still reads no environment it was not told to read, and a hook
84
+ * generated with no `observeNames` behaves exactly as it did before.
33
85
  */
34
- export function renderTurnEndHook(turnEndFile) {
86
+ export function renderTurnEndHook(turnEndFile, observeNames) {
87
+ const observing = observeNames !== undefined && observeNames.length > 0;
88
+ const envLines = observing
89
+ ? `const observed = {};
90
+ for (const name of ${JSON.stringify([...observeNames])}) {
91
+ const value = process.env[name];
92
+ observed[name] = typeof value === "string" ? value : null;
93
+ }
94
+ record.env = observed;
95
+ `
96
+ : "";
35
97
  return `#!/usr/bin/env node
36
98
  // Generated by tiphys spawn (kernel plan v1, M1-P4 step 3). Invoked by the
37
99
  // executor adapter when the payload command exits, with the payload's exit
@@ -46,16 +108,17 @@ if (!Number.isInteger(exitCode)) {
46
108
  );
47
109
  process.exit(64);
48
110
  }
49
- writeFileSync(
111
+ const record = { endedAt: new Date().toISOString(), exitCode };
112
+ ${envLines}writeFileSync(
50
113
  ${JSON.stringify(turnEndFile)},
51
- \`\${JSON.stringify({ endedAt: new Date().toISOString(), exitCode }, null, 2)}\\n\`,
114
+ \`\${JSON.stringify(record, null, 2)}\\n\`,
52
115
  );
53
116
  `;
54
117
  }
55
118
  /** Write the hook for a task and return its path. */
56
- export function writeTurnEndHook(fleet, taskId) {
119
+ export function writeTurnEndHook(fleet, taskId, observeNames) {
57
120
  const path = turnEndHookPath(fleet, taskId);
58
- writeFileSync(path, renderTurnEndHook(turnEndPath(fleet, taskId)), {
121
+ writeFileSync(path, renderTurnEndHook(turnEndPath(fleet, taskId), observeNames), {
59
122
  mode: 0o755,
60
123
  });
61
124
  return path;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * THE KERNEL'S PUBLIC ENTRY POINT (kernel plan M4, M4-P4 criteria 7 and 8).
3
+ *
4
+ * Until this phase `@tiphys/kernel` declared `bin` and nothing else
5
+ * (package.json:14): no `main`, no `exports`, no `types`, and no
6
+ * `src/index.ts`. A separately published plugin could therefore not import
7
+ * `ExecutorAdapter` at all, which made the executor seam unreachable from
8
+ * outside this repository however well the interface was written.
9
+ *
10
+ * WHAT IS PUBLISHED HERE IS A COMMITMENT AND IS DELIBERATELY SMALL. An
11
+ * `exports` map is a semver promise and a patch release cannot take one back,
12
+ * so this file names the adapter CONTRACT and nothing else. In particular it
13
+ * does not export `spawnTask`, the fleet accessors, the gate runner or
14
+ * anything under `src/exec/`: those are the kernel's internals, they change
15
+ * without notice, and `package.json`'s `exports` map has no wildcard subpath
16
+ * precisely so that they stay unreachable through the package name
17
+ * (criterion 8, whose dangerous state is an `exports` map written
18
+ * `"./*": "./dist/src/*"`, which satisfies criterion 7 and publishes the
19
+ * entire kernel as API).
20
+ *
21
+ * THE TWO VALUES ARE PART OF THE CONTRACT, not a convenience. An adapter
22
+ * author has to answer two questions that types cannot answer at runtime:
23
+ * which names may appear in `requires` (the closed set the kernel checks
24
+ * against, src/spawn.ts's `requirableRequestFields`), and which name is
25
+ * reserved for the built-in adapter (`BUILT_IN_ADAPTER_NAME`, refused by the
26
+ * loader). Publishing the answers is cheaper than publishing a document that
27
+ * drifts from them.
28
+ */
29
+ export type { ExecutorAdapter, ExecutorRecord, ExecutorRequest, LaunchOutcome, } from "./spawn.ts";
30
+ export { requirableRequestFields } from "./spawn.ts";
31
+ export { BUILT_IN_ADAPTER_NAME } from "./adapters/load.ts";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * THE KERNEL'S PUBLIC ENTRY POINT (kernel plan M4, M4-P4 criteria 7 and 8).
3
+ *
4
+ * Until this phase `@tiphys/kernel` declared `bin` and nothing else
5
+ * (package.json:14): no `main`, no `exports`, no `types`, and no
6
+ * `src/index.ts`. A separately published plugin could therefore not import
7
+ * `ExecutorAdapter` at all, which made the executor seam unreachable from
8
+ * outside this repository however well the interface was written.
9
+ *
10
+ * WHAT IS PUBLISHED HERE IS A COMMITMENT AND IS DELIBERATELY SMALL. An
11
+ * `exports` map is a semver promise and a patch release cannot take one back,
12
+ * so this file names the adapter CONTRACT and nothing else. In particular it
13
+ * does not export `spawnTask`, the fleet accessors, the gate runner or
14
+ * anything under `src/exec/`: those are the kernel's internals, they change
15
+ * without notice, and `package.json`'s `exports` map has no wildcard subpath
16
+ * precisely so that they stay unreachable through the package name
17
+ * (criterion 8, whose dangerous state is an `exports` map written
18
+ * `"./*": "./dist/src/*"`, which satisfies criterion 7 and publishes the
19
+ * entire kernel as API).
20
+ *
21
+ * THE TWO VALUES ARE PART OF THE CONTRACT, not a convenience. An adapter
22
+ * author has to answer two questions that types cannot answer at runtime:
23
+ * which names may appear in `requires` (the closed set the kernel checks
24
+ * against, src/spawn.ts's `requirableRequestFields`), and which name is
25
+ * reserved for the built-in adapter (`BUILT_IN_ADAPTER_NAME`, refused by the
26
+ * loader). Publishing the answers is cheaper than publishing a document that
27
+ * drifts from them.
28
+ */
29
+ export { requirableRequestFields } from "./spawn.js";
30
+ export { BUILT_IN_ADAPTER_NAME } from "./adapters/load.js";
@@ -51,9 +51,19 @@
51
51
  *
52
52
  * Exclusion domain (PR-201, DR-0007 stated honestly): the lease excludes
53
53
  * within one filesystem and one clock, the fleet home the lock file lives
54
- * in. Cross-environment exclusion for a fleet shared through a git remote
55
- * is M4 residue and is not claimed here. Mutations of the lock file made
56
- * outside this module (manual edits) are not covered by the contract.
54
+ * in. Mutations of the lock file made outside this module (manual edits)
55
+ * are not covered by the contract.
56
+ *
57
+ * CROSS-ENVIRONMENT EXCLUSION IS A SECOND LAYER ABOVE THIS ONE (M4-P21),
58
+ * and it is OFF unless the fleet home declares it. When the fleet's own
59
+ * `package.json` carries `tiphys.sharedExclusion`, every mutation below
60
+ * first asks `src/exclusion.ts` for a verdict from the shared register on
61
+ * the fleet's git remote, and only a won verdict reaches the local lease.
62
+ * With the field ABSENT, `readSharedExclusion` returns before spawning
63
+ * anything and every path in this module behaves exactly as it did, which
64
+ * is the property M4-P21 criterion 1 asserts. The refusal never touches the
65
+ * lock file, which is what makes the fail-closed behaviour on an
66
+ * unreachable register observable (criterion 7): no local lease appears.
57
67
  *
58
68
  * Renewal discipline (PR-203): the default lease lasts 900 seconds and
59
69
  * the holder renews at or before half-life (renewByMs). Holdership on
@@ -77,15 +87,39 @@ export type ObservedLease = {
77
87
  raw: string;
78
88
  lease: Lease | undefined;
79
89
  };
90
+ /**
91
+ * What the shared exclusion layer decided. Present only when the fleet home
92
+ * declares the layer; the CLI prints `line` verbatim, and `line` always
93
+ * names which of the two staleness bases reached the verdict, which is
94
+ * M4-P21 criterion 6.
95
+ *
96
+ * THE VERDICT IS CARRIED AS TEXT ON PURPOSE, not as a discriminated field.
97
+ * The C-2 structural inspection over this file (test/lock.test.ts:534,
98
+ * M1-P3 criterion 10) forbids a whole vocabulary of process-probing words
99
+ * from `src/lock.ts`, and it is a blunt case-insensitive grep, which is the
100
+ * property that makes it hard to defeat by accident. Naming the field after
101
+ * that vocabulary would have reddened it for a reason that has nothing to do
102
+ * with C-2, and widening the grep to let this through would weaken a guard
103
+ * this module is the whole reason for. The classification itself lives in
104
+ * `src/exclusion.ts`, which no such grep covers, and any caller needing it
105
+ * as a value reads it there.
106
+ */
107
+ export interface SharedNote {
108
+ line: string;
109
+ envId: string;
110
+ }
80
111
  export type LeaseOutcome = {
81
112
  ok: true;
82
113
  lease: Lease;
114
+ shared?: SharedNote;
83
115
  } | {
84
116
  ok: true;
85
117
  lease: null;
118
+ shared?: SharedNote;
86
119
  } | {
87
120
  ok: false;
88
121
  reason: string;
122
+ shared?: SharedNote;
89
123
  /**
90
124
  * True when the operation failed because a mutation claim file
91
125
  * was still present after the bounded wait (CR-204). A stale
@@ -97,8 +131,40 @@ export type LeaseOutcome = {
97
131
  claimTimeout?: boolean;
98
132
  };
99
133
  export declare function renderLease(lease: Lease): string;
100
- /** Read the current lock file state: absent, or present with raw bytes. */
134
+ /**
135
+ * Read the current lock file state: absent, or present with raw bytes.
136
+ *
137
+ * THE ENTRY TYPE IS ESTABLISHED BEFORE THE OPEN (T-008's shape in shipped
138
+ * code). A bare `readFileSync` here blocked FOREVER with zero output on a
139
+ * named pipe at the lease path, and took `lock status`, `lock acquire`,
140
+ * `lock renew` and `lock release` with it, while `tiphys doctor` returned in
141
+ * the same second against the same FIFO with "is a named pipe, not a regular
142
+ * file, so it was not opened". Two readers of one path, one of which
143
+ * established the type; this is now the same reader.
144
+ *
145
+ * A refusal THROWS rather than returning a fourth `ObservedLease` variant.
146
+ * The function already threw on every non-ENOENT error, so the contract its
147
+ * callers were written against is unchanged, and bin/tiphys.ts turns the
148
+ * throw into one diagnostic line and a nonzero exit.
149
+ */
101
150
  export declare function observeLease(lockPath: string): ObservedLease;
151
+ /**
152
+ * THE ONE EXPIRY COMPARISON IN THE KERNEL (M4-P17 criterion 2).
153
+ *
154
+ * `isExpired` below needs a whole `Lease`, and doctor's lock check does not
155
+ * have one: it reads the lease file defensively and holds only `holderId` and
156
+ * `expiresAt`, because a lease file that fails `parseLease` must still produce
157
+ * a diagnosis rather than nothing. Before this phase that forced doctor to
158
+ * carry its own `Date.parse(...) <= Date.now()`, which is a SECOND comparison
159
+ * of the same property, free to drift from this one. The boundary is where
160
+ * that drift shows: `<=` makes expiry INCLUSIVE, so a lease whose `expiresAt`
161
+ * is exactly the current millisecond is expired, and a second copy written
162
+ * with `<` disagrees for exactly one millisecond and agrees everywhere else.
163
+ * A disagreement that narrow is not something a reviewer finds by reading.
164
+ *
165
+ * So the comparison lives here once and both callers reach it.
166
+ */
167
+ export declare function expiryHasPassed(expiresAt: string, nowMs: number): boolean;
102
168
  export declare function isExpired(lease: Lease, nowMs: number): boolean;
103
169
  /** The renew-by deadline (half-life of the current term), for holders. */
104
170
  export declare function renewByMs(lease: Lease): number;
@@ -126,6 +192,12 @@ export interface AcquireOptions {
126
192
  takeover?: boolean;
127
193
  durationSeconds?: number;
128
194
  nowMs?: number;
195
+ /**
196
+ * The fleet home this lock belongs to. Derived from lockPath when absent;
197
+ * named explicitly only by callers whose lock path is not the fleet's
198
+ * canonical one.
199
+ */
200
+ fleetRoot?: string;
129
201
  /**
130
202
  * Staging seam for deterministic race witnesses: the decision is made
131
203
  * against this pre-observed state instead of a fresh read, and the
@@ -142,6 +214,8 @@ export declare function acquireLease(lockPath: string, options?: AcquireOptions)
142
214
  export interface RenewOptions {
143
215
  durationSeconds?: number;
144
216
  nowMs?: number;
217
+ /** See AcquireOptions.fleetRoot. */
218
+ fleetRoot?: string;
145
219
  /** Staging seam for deterministic race witnesses; see AcquireOptions. */
146
220
  observed?: ObservedLease;
147
221
  }
@@ -156,6 +230,10 @@ export declare function renewLease(lockPath: string, holderId: string, options?:
156
230
  export interface ReleaseOptions {
157
231
  /** Staging seam for deterministic race witnesses; see AcquireOptions. */
158
232
  observed?: ObservedLease;
233
+ /** See AcquireOptions.fleetRoot. */
234
+ fleetRoot?: string;
235
+ /** Decision clock, for the shared layer. Defaults to Date.now(). */
236
+ nowMs?: number;
159
237
  }
160
238
  /**
161
239
  * Release the lease held by holderId. Expiry does not block a release