@tiphys/kernel 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +611 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +278 -0
- package/checklists/clean-room.yaml +325 -0
- package/checklists/env-failure-diagnosis.yaml +68 -0
- package/checklists/flake-playbook.yaml +68 -0
- package/checklists/hazard-review.yaml +144 -0
- package/checklists/plan-review.yaml +103 -0
- package/dist/bin/tiphys.d.ts +2 -0
- package/dist/bin/tiphys.js +14 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +59 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +828 -0
- package/dist/src/checks.js +3314 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +57 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +320 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/doctor.d.ts +35 -0
- package/dist/src/commands/doctor.js +605 -0
- package/dist/src/commands/gates.d.ts +9 -0
- package/dist/src/commands/gates.js +360 -0
- package/dist/src/commands/init.d.ts +30 -0
- package/dist/src/commands/init.js +128 -0
- package/dist/src/commands/lock.d.ts +1 -0
- package/dist/src/commands/lock.js +229 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -0
- package/dist/src/commands/plan.d.ts +20 -0
- package/dist/src/commands/plan.js +105 -0
- package/dist/src/commands/pool.d.ts +1 -0
- package/dist/src/commands/pool.js +128 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +146 -0
- package/dist/src/commands/status.d.ts +19 -0
- package/dist/src/commands/status.js +120 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +79 -0
- package/dist/src/commands/tuition.d.ts +21 -0
- package/dist/src/commands/tuition.js +218 -0
- package/dist/src/commands/validate.d.ts +78 -0
- package/dist/src/commands/validate.js +360 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/exec/env.d.ts +128 -0
- package/dist/src/exec/env.js +190 -0
- package/dist/src/fleet.d.ts +51 -0
- package/dist/src/fleet.js +80 -0
- package/dist/src/gates/adapters/http-json.d.ts +5 -0
- package/dist/src/gates/adapters/http-json.js +283 -0
- package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
- package/dist/src/gates/adapters/migrations-command.js +373 -0
- package/dist/src/gates/citations.d.ts +408 -0
- package/dist/src/gates/citations.js +1163 -0
- package/dist/src/gates/coverage.d.ts +284 -0
- package/dist/src/gates/coverage.js +701 -0
- package/dist/src/gates/credentials.d.ts +74 -0
- package/dist/src/gates/credentials.js +533 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -0
- package/dist/src/gates/migrations.d.ts +1 -0
- package/dist/src/gates/migrations.js +36 -0
- package/dist/src/gates/pin.d.ts +114 -0
- package/dist/src/gates/pin.js +154 -0
- package/dist/src/gates/red-witness.d.ts +22 -0
- package/dist/src/gates/red-witness.js +390 -0
- package/dist/src/gates/release.d.ts +283 -0
- package/dist/src/gates/release.js +820 -0
- package/dist/src/gates/result.d.ts +116 -0
- package/dist/src/gates/result.js +91 -0
- package/dist/src/gates/run.d.ts +566 -0
- package/dist/src/gates/run.js +1536 -0
- package/dist/src/gates/schemas/citation-config.schema.json +59 -0
- package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
- package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
- package/dist/src/gates/schemas/gate-result.schema.json +160 -0
- package/dist/src/gates/schemas/phase-declaration.schema.json +42 -0
- package/dist/src/gates/schemas/release-record.schema.json +119 -0
- package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
- package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
- package/dist/src/gates/scope.d.ts +131 -0
- package/dist/src/gates/scope.js +1018 -0
- package/dist/src/gates/suite.d.ts +217 -0
- package/dist/src/gates/suite.js +927 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +32 -0
- package/dist/src/hooks.js +62 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +178 -0
- package/dist/src/lock.js +500 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +2 -0
- package/dist/src/path-identity.js +10 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +130 -0
- package/dist/src/pool.js +721 -0
- package/dist/src/roles.d.ts +430 -0
- package/dist/src/roles.js +734 -0
- package/dist/src/spawn.d.ts +177 -0
- package/dist/src/spawn.js +332 -0
- package/dist/src/status.d.ts +91 -0
- package/dist/src/status.js +119 -0
- package/dist/src/task.d.ts +264 -0
- package/dist/src/task.js +305 -0
- package/dist/src/teardown.d.ts +32 -0
- package/dist/src/teardown.js +314 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +230 -0
- package/dist/src/validate.js +732 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.js +38 -0
- package/dist/src/watcher.d.ts +275 -0
- package/dist/src/watcher.js +859 -0
- package/dist/src/witness/run.d.ts +274 -0
- package/dist/src/witness/run.js +1327 -0
- package/dist/src/witness/spec.d.ts +102 -0
- package/dist/src/witness/spec.js +253 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +390 -0
- package/gates.manifest.json +195 -0
- package/package.json +57 -3
- package/role-model-config.yaml +88 -0
- package/roles/README.md +128 -0
- package/roles/_shared-dispatch-contract.md +87 -0
- package/roles/adversarial-plan-reviewer.md +80 -0
- package/roles/clean-room-reviewer.md +140 -0
- package/roles/implementer.md +460 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +81 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +166 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/decision-record.schema.json +88 -0
- package/schemas/final-report.schema.json +90 -0
- package/schemas/finding.schema.json +106 -0
- package/schemas/gate-registry.schema.json +260 -0
- package/schemas/mechanism-index.schema.json +94 -0
- package/schemas/plan.schema.json +300 -0
- package/schemas/report.schema.json +579 -0
- package/schemas/role-brief.schema.json +105 -0
- package/schemas/role-model-config.schema.json +90 -0
- package/schemas/status-line.schema.json +40 -0
- package/schemas/tuition.schema.json +191 -0
- package/schemas/verdict.schema.json +289 -0
- package/schemas/work-history.schema.json +183 -0
- package/templates/charter.example.yaml +54 -0
- package/templates/decision-record.example.yaml +27 -0
- package/templates/final-report.example.yaml +80 -0
- package/templates/plan.example.yaml +87 -0
- package/templates/report.example.yaml +236 -0
- package/templates/warnings.md +74 -0
- package/templates/work-history.example.yaml +185 -0
- package/tuition/README.md +76 -0
- package/tuition/T-001.yaml +48 -0
- package/tuition/T-002.yaml +51 -0
- package/tuition/T-003.yaml +100 -0
- package/tuition/T-004.yaml +52 -0
- package/tuition/T-005.yaml +72 -0
- package/tuition/T-006.yaml +81 -0
- package/tuition/T-007.yaml +56 -0
- package/tuition/T-008.yaml +111 -0
- package/tuition/T-009.yaml +50 -0
- package/tuition/T-015.yaml +36 -0
- package/tuition/T-016.yaml +36 -0
- package/tuition/T-017.yaml +46 -0
- package/tuition/T-018.yaml +84 -0
- package/tuition/T-021.yaml +40 -0
- package/tuition/T-022.yaml +36 -0
- package/tuition/mechanism-index.yaml +256 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CHILD-ENVIRONMENT CONSTRUCTION (kernel plan M2, M2-P8 steps 2 and 3).
|
|
3
|
+
*
|
|
4
|
+
* Every child the executor launches (the payload and the turn-end hook)
|
|
5
|
+
* receives an environment BUILT here, never inherited. The build is an
|
|
6
|
+
* ALLOWLIST (M2-D-13): a variable crosses into the child only when its
|
|
7
|
+
* exact name is in the list below or in the caller's per-invocation
|
|
8
|
+
* extension. There is no denylist anywhere in this module, because a
|
|
9
|
+
* denylist is a union of the credential names known on the day it was
|
|
10
|
+
* written and grows stale silently; an allowlist fails in the safe
|
|
11
|
+
* direction (a missing variable is a visible breakage, a leaked one is
|
|
12
|
+
* not).
|
|
13
|
+
*
|
|
14
|
+
* REDIRECT, NEVER DROP, THE CREDENTIAL-STORE POINTERS (M2R-004 edit 1).
|
|
15
|
+
* `HOME`, `XDG_CONFIG_HOME`, `GH_CONFIG_DIR`, `GIT_CONFIG_GLOBAL` and
|
|
16
|
+
* `GIT_CONFIG_SYSTEM` are not simply excluded from the allowlist; they are
|
|
17
|
+
* SET to harness-owned empty paths inside the task directory. Dropping any
|
|
18
|
+
* of them is FORBIDDEN, and this comment is the record of why:
|
|
19
|
+
*
|
|
20
|
+
* - A dropped `HOME` does not remove the home directory; the child's
|
|
21
|
+
* tools fall back to the real one (or to `/root`), so gh resolves
|
|
22
|
+
* `$HOME/.config/gh/hosts.yml`, git resolves `~/.gitconfig`,
|
|
23
|
+
* `~/.netrc` and `~/.git-credentials`, and every default credential
|
|
24
|
+
* path RETURNS. An environment-only scrub that drops pointers leaves
|
|
25
|
+
* the credential stores in place, which is exactly finding M2R-004.
|
|
26
|
+
* - A dropped `GIT_CONFIG_GLOBAL` additionally hands the child the
|
|
27
|
+
* user's real global git configuration, undoing the EXT-F-02
|
|
28
|
+
* discipline (v1 M1-P2 criterion 7): the kernel never reads and never
|
|
29
|
+
* writes user or global git config, and commit identity crosses as
|
|
30
|
+
* command-scoped GIT_AUTHOR_* / GIT_COMMITTER_* variables only.
|
|
31
|
+
*
|
|
32
|
+
* THE ALLOWLIST IS DATA, NOT A LITERAL INSIDE A SPAWN CALL (step 9). It is
|
|
33
|
+
* exported and `buildChildEnv` takes a per-invocation `extraAllowlist`, so
|
|
34
|
+
* a future caller (the M4-era release-verification wiring designed in
|
|
35
|
+
* delivery/verification/release-verification-interface.md section 6.1)
|
|
36
|
+
* can extend it per invocation and per adapter without touching this
|
|
37
|
+
* module. Nothing in this module knows about release verification; that
|
|
38
|
+
* is deliberate and this phase builds no such awareness.
|
|
39
|
+
*
|
|
40
|
+
* WHY THE THREE TIPHYS_EXIT_TEST_* NAMES ARE HERE. The delivered payload
|
|
41
|
+
* contract (scripts/stub-payload.sh, its documented environment contract)
|
|
42
|
+
* is the one place variables cross the spawn boundary into a payload
|
|
43
|
+
* today, and it names exactly these three. They are listed as exact names
|
|
44
|
+
* rather than as a TIPHYS_* prefix on purpose: a prefix rule would carry
|
|
45
|
+
* ANY variable that happens to start with TIPHYS_, which is a denylist's
|
|
46
|
+
* failure mode wearing an allowlist's name (and is what acceptance
|
|
47
|
+
* criterion 2's TIPHYS_UNRELATED_SECRET witness refutes).
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* The default allowlist. Exact names only; no prefixes, no patterns.
|
|
51
|
+
* Append here only with a recorded reason, and never a credential-capable
|
|
52
|
+
* name. This allowlist is the REAL defense: a name crosses only if it is
|
|
53
|
+
* here (or in a per-invocation extension), so nothing outside it appears
|
|
54
|
+
* in a child by construction. `credential-scrub` (src/gates/credentials.ts)
|
|
55
|
+
* adds a bounded, allowlist-INDEPENDENT tripwire on top: it reddens if a
|
|
56
|
+
* gh-documented token variable OR a git/ssh/node credential- or
|
|
57
|
+
* code-execution-capable variable (its documented vocabulary) is present
|
|
58
|
+
* in the constructed child, even if some future edit wrongly adds one
|
|
59
|
+
* here. That tripwire is a bounded denylist and cannot enumerate every
|
|
60
|
+
* dangerous name; it makes a widened allowlist cost a red for the names it
|
|
61
|
+
* knows, it does not replace this allowlist.
|
|
62
|
+
*/
|
|
63
|
+
export declare const DEFAULT_CHILD_ENV_ALLOWLIST: readonly string[];
|
|
64
|
+
/** One redirected credential-store pointer. */
|
|
65
|
+
export interface CredentialRedirection {
|
|
66
|
+
/** The environment variable name. */
|
|
67
|
+
name: string;
|
|
68
|
+
/** Whether the harness-owned target is a directory or a file. */
|
|
69
|
+
kind: "directory" | "file";
|
|
70
|
+
/** Path of the target relative to the scrub root. */
|
|
71
|
+
relativePath: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The five pointers, redirected in this order. Each names a credential
|
|
75
|
+
* store's location; see the module comment for why none may be dropped.
|
|
76
|
+
*/
|
|
77
|
+
export declare const CREDENTIAL_STORE_REDIRECTIONS: readonly CredentialRedirection[];
|
|
78
|
+
/** Directory name of the scrub root inside a task directory. */
|
|
79
|
+
export declare const SCRUB_DIR_NAME = "scrub-env";
|
|
80
|
+
/** Where spawn puts the harness-owned redirect targets for a task. */
|
|
81
|
+
export declare function scrubRoot(taskDir: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* Every variable name the constructed environment may contain: the
|
|
84
|
+
* allowlist, the per-invocation extension, and the redirected pointers.
|
|
85
|
+
*/
|
|
86
|
+
export declare function permittedChildEnvNames(extraAllowlist?: readonly string[]): Set<string>;
|
|
87
|
+
export interface ChildEnvSpec {
|
|
88
|
+
/** The environment the values are copied FROM (usually process.env). */
|
|
89
|
+
parentEnv: Record<string, string | undefined>;
|
|
90
|
+
/**
|
|
91
|
+
* Absolute path of the harness-owned scrub root. Created (with its five
|
|
92
|
+
* redirect targets) by this call; must sit inside a directory this
|
|
93
|
+
* invocation owns, which for spawn is the task directory.
|
|
94
|
+
*/
|
|
95
|
+
scrubDir: string;
|
|
96
|
+
/**
|
|
97
|
+
* Per-invocation allowlist extension (step 9's only obligation to the
|
|
98
|
+
* future). Exact names, same semantics as the default list.
|
|
99
|
+
*/
|
|
100
|
+
extraAllowlist?: readonly string[];
|
|
101
|
+
}
|
|
102
|
+
export type ChildEnvResult = {
|
|
103
|
+
ok: true;
|
|
104
|
+
env: Record<string, string>;
|
|
105
|
+
} | {
|
|
106
|
+
ok: false;
|
|
107
|
+
reason: string;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Build the child environment: create the scrub root and its five EMPTY
|
|
111
|
+
* redirect targets, copy the allowlisted names that are present in the
|
|
112
|
+
* parent environment, then OVERRIDE the five pointers with the
|
|
113
|
+
* harness-owned paths. The override runs last and unconditionally, so a
|
|
114
|
+
* pointer that is also (wrongly) allowlisted still ends up redirected.
|
|
115
|
+
*
|
|
116
|
+
* EVERY redirect target is re-emptied on every build, directory and file
|
|
117
|
+
* alike: a leftover store file from a prior same-taskId incarnation (a
|
|
118
|
+
* .git-credentials or hosts.yml written into a directory target, or a
|
|
119
|
+
* populated gitconfig-global file) must not survive a rebuild and smuggle
|
|
120
|
+
* configuration into the next child (M2-P8 fix round 1, review finding O1).
|
|
121
|
+
* File targets are rewritten empty; directory targets are removed and
|
|
122
|
+
* recreated so they are empty too.
|
|
123
|
+
*
|
|
124
|
+
* A failure to stage any redirect target fails the whole construction:
|
|
125
|
+
* an unredirected pointer would silently fall back to the default
|
|
126
|
+
* credential path, so there is no partial success here (fail closed).
|
|
127
|
+
*/
|
|
128
|
+
export declare function buildChildEnv(spec: ChildEnvSpec): ChildEnvResult;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { refuseOpenForWrite, runStep } from "../task.js";
|
|
4
|
+
/**
|
|
5
|
+
* CHILD-ENVIRONMENT CONSTRUCTION (kernel plan M2, M2-P8 steps 2 and 3).
|
|
6
|
+
*
|
|
7
|
+
* Every child the executor launches (the payload and the turn-end hook)
|
|
8
|
+
* receives an environment BUILT here, never inherited. The build is an
|
|
9
|
+
* ALLOWLIST (M2-D-13): a variable crosses into the child only when its
|
|
10
|
+
* exact name is in the list below or in the caller's per-invocation
|
|
11
|
+
* extension. There is no denylist anywhere in this module, because a
|
|
12
|
+
* denylist is a union of the credential names known on the day it was
|
|
13
|
+
* written and grows stale silently; an allowlist fails in the safe
|
|
14
|
+
* direction (a missing variable is a visible breakage, a leaked one is
|
|
15
|
+
* not).
|
|
16
|
+
*
|
|
17
|
+
* REDIRECT, NEVER DROP, THE CREDENTIAL-STORE POINTERS (M2R-004 edit 1).
|
|
18
|
+
* `HOME`, `XDG_CONFIG_HOME`, `GH_CONFIG_DIR`, `GIT_CONFIG_GLOBAL` and
|
|
19
|
+
* `GIT_CONFIG_SYSTEM` are not simply excluded from the allowlist; they are
|
|
20
|
+
* SET to harness-owned empty paths inside the task directory. Dropping any
|
|
21
|
+
* of them is FORBIDDEN, and this comment is the record of why:
|
|
22
|
+
*
|
|
23
|
+
* - A dropped `HOME` does not remove the home directory; the child's
|
|
24
|
+
* tools fall back to the real one (or to `/root`), so gh resolves
|
|
25
|
+
* `$HOME/.config/gh/hosts.yml`, git resolves `~/.gitconfig`,
|
|
26
|
+
* `~/.netrc` and `~/.git-credentials`, and every default credential
|
|
27
|
+
* path RETURNS. An environment-only scrub that drops pointers leaves
|
|
28
|
+
* the credential stores in place, which is exactly finding M2R-004.
|
|
29
|
+
* - A dropped `GIT_CONFIG_GLOBAL` additionally hands the child the
|
|
30
|
+
* user's real global git configuration, undoing the EXT-F-02
|
|
31
|
+
* discipline (v1 M1-P2 criterion 7): the kernel never reads and never
|
|
32
|
+
* writes user or global git config, and commit identity crosses as
|
|
33
|
+
* command-scoped GIT_AUTHOR_* / GIT_COMMITTER_* variables only.
|
|
34
|
+
*
|
|
35
|
+
* THE ALLOWLIST IS DATA, NOT A LITERAL INSIDE A SPAWN CALL (step 9). It is
|
|
36
|
+
* exported and `buildChildEnv` takes a per-invocation `extraAllowlist`, so
|
|
37
|
+
* a future caller (the M4-era release-verification wiring designed in
|
|
38
|
+
* delivery/verification/release-verification-interface.md section 6.1)
|
|
39
|
+
* can extend it per invocation and per adapter without touching this
|
|
40
|
+
* module. Nothing in this module knows about release verification; that
|
|
41
|
+
* is deliberate and this phase builds no such awareness.
|
|
42
|
+
*
|
|
43
|
+
* WHY THE THREE TIPHYS_EXIT_TEST_* NAMES ARE HERE. The delivered payload
|
|
44
|
+
* contract (scripts/stub-payload.sh, its documented environment contract)
|
|
45
|
+
* is the one place variables cross the spawn boundary into a payload
|
|
46
|
+
* today, and it names exactly these three. They are listed as exact names
|
|
47
|
+
* rather than as a TIPHYS_* prefix on purpose: a prefix rule would carry
|
|
48
|
+
* ANY variable that happens to start with TIPHYS_, which is a denylist's
|
|
49
|
+
* failure mode wearing an allowlist's name (and is what acceptance
|
|
50
|
+
* criterion 2's TIPHYS_UNRELATED_SECRET witness refutes).
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* The default allowlist. Exact names only; no prefixes, no patterns.
|
|
54
|
+
* Append here only with a recorded reason, and never a credential-capable
|
|
55
|
+
* name. This allowlist is the REAL defense: a name crosses only if it is
|
|
56
|
+
* here (or in a per-invocation extension), so nothing outside it appears
|
|
57
|
+
* in a child by construction. `credential-scrub` (src/gates/credentials.ts)
|
|
58
|
+
* adds a bounded, allowlist-INDEPENDENT tripwire on top: it reddens if a
|
|
59
|
+
* gh-documented token variable OR a git/ssh/node credential- or
|
|
60
|
+
* code-execution-capable variable (its documented vocabulary) is present
|
|
61
|
+
* in the constructed child, even if some future edit wrongly adds one
|
|
62
|
+
* here. That tripwire is a bounded denylist and cannot enumerate every
|
|
63
|
+
* dangerous name; it makes a widened allowlist cost a red for the names it
|
|
64
|
+
* knows, it does not replace this allowlist.
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_CHILD_ENV_ALLOWLIST = [
|
|
67
|
+
// Program resolution and scratch space.
|
|
68
|
+
"PATH",
|
|
69
|
+
"TMPDIR",
|
|
70
|
+
// Locale, so a child's tool output is not re-localised mid-suite.
|
|
71
|
+
"LANG",
|
|
72
|
+
"LANGUAGE",
|
|
73
|
+
"LC_ALL",
|
|
74
|
+
"LC_COLLATE",
|
|
75
|
+
"LC_CTYPE",
|
|
76
|
+
"LC_MESSAGES",
|
|
77
|
+
"LC_MONETARY",
|
|
78
|
+
"LC_NUMERIC",
|
|
79
|
+
"LC_TIME",
|
|
80
|
+
// The payload contract of the exit-test harness (see module comment).
|
|
81
|
+
"TIPHYS_EXIT_TEST_MODE",
|
|
82
|
+
"TIPHYS_EXIT_TEST_TASK",
|
|
83
|
+
"TIPHYS_EXIT_TEST_REPORT",
|
|
84
|
+
// Command-scoped git identity (EXT-F-02 option B): the ONLY sanctioned
|
|
85
|
+
// way identity reaches a child, and never a credential.
|
|
86
|
+
"GIT_AUTHOR_NAME",
|
|
87
|
+
"GIT_AUTHOR_EMAIL",
|
|
88
|
+
"GIT_AUTHOR_DATE",
|
|
89
|
+
"GIT_COMMITTER_NAME",
|
|
90
|
+
"GIT_COMMITTER_EMAIL",
|
|
91
|
+
"GIT_COMMITTER_DATE",
|
|
92
|
+
];
|
|
93
|
+
/**
|
|
94
|
+
* The five pointers, redirected in this order. Each names a credential
|
|
95
|
+
* store's location; see the module comment for why none may be dropped.
|
|
96
|
+
*/
|
|
97
|
+
export const CREDENTIAL_STORE_REDIRECTIONS = [
|
|
98
|
+
{ name: "HOME", kind: "directory", relativePath: "home" },
|
|
99
|
+
{ name: "XDG_CONFIG_HOME", kind: "directory", relativePath: "xdg-config" },
|
|
100
|
+
{ name: "GH_CONFIG_DIR", kind: "directory", relativePath: "gh-config" },
|
|
101
|
+
{ name: "GIT_CONFIG_GLOBAL", kind: "file", relativePath: "gitconfig-global" },
|
|
102
|
+
{ name: "GIT_CONFIG_SYSTEM", kind: "file", relativePath: "gitconfig-system" },
|
|
103
|
+
];
|
|
104
|
+
/** Directory name of the scrub root inside a task directory. */
|
|
105
|
+
export const SCRUB_DIR_NAME = "scrub-env";
|
|
106
|
+
/** Where spawn puts the harness-owned redirect targets for a task. */
|
|
107
|
+
export function scrubRoot(taskDir) {
|
|
108
|
+
return join(taskDir, SCRUB_DIR_NAME);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Every variable name the constructed environment may contain: the
|
|
112
|
+
* allowlist, the per-invocation extension, and the redirected pointers.
|
|
113
|
+
*/
|
|
114
|
+
export function permittedChildEnvNames(extraAllowlist = []) {
|
|
115
|
+
return new Set([
|
|
116
|
+
...DEFAULT_CHILD_ENV_ALLOWLIST,
|
|
117
|
+
...extraAllowlist,
|
|
118
|
+
...CREDENTIAL_STORE_REDIRECTIONS.map((redirection) => redirection.name),
|
|
119
|
+
"GIT_CONFIG_NOSYSTEM",
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Build the child environment: create the scrub root and its five EMPTY
|
|
124
|
+
* redirect targets, copy the allowlisted names that are present in the
|
|
125
|
+
* parent environment, then OVERRIDE the five pointers with the
|
|
126
|
+
* harness-owned paths. The override runs last and unconditionally, so a
|
|
127
|
+
* pointer that is also (wrongly) allowlisted still ends up redirected.
|
|
128
|
+
*
|
|
129
|
+
* EVERY redirect target is re-emptied on every build, directory and file
|
|
130
|
+
* alike: a leftover store file from a prior same-taskId incarnation (a
|
|
131
|
+
* .git-credentials or hosts.yml written into a directory target, or a
|
|
132
|
+
* populated gitconfig-global file) must not survive a rebuild and smuggle
|
|
133
|
+
* configuration into the next child (M2-P8 fix round 1, review finding O1).
|
|
134
|
+
* File targets are rewritten empty; directory targets are removed and
|
|
135
|
+
* recreated so they are empty too.
|
|
136
|
+
*
|
|
137
|
+
* A failure to stage any redirect target fails the whole construction:
|
|
138
|
+
* an unredirected pointer would silently fall back to the default
|
|
139
|
+
* credential path, so there is no partial success here (fail closed).
|
|
140
|
+
*/
|
|
141
|
+
export function buildChildEnv(spec) {
|
|
142
|
+
const made = runStep(`creating the scrub root ${spec.scrubDir}`, () => mkdirSync(spec.scrubDir, { recursive: true }));
|
|
143
|
+
if (!made.ok) {
|
|
144
|
+
return { ok: false, reason: made.reason };
|
|
145
|
+
}
|
|
146
|
+
const env = {};
|
|
147
|
+
const names = [
|
|
148
|
+
...DEFAULT_CHILD_ENV_ALLOWLIST,
|
|
149
|
+
...(spec.extraAllowlist ?? []),
|
|
150
|
+
];
|
|
151
|
+
for (const name of names) {
|
|
152
|
+
const value = spec.parentEnv[name];
|
|
153
|
+
if (value !== undefined) {
|
|
154
|
+
env[name] = value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const redirection of CREDENTIAL_STORE_REDIRECTIONS) {
|
|
158
|
+
const target = join(spec.scrubDir, redirection.relativePath);
|
|
159
|
+
if (redirection.kind === "directory") {
|
|
160
|
+
// Re-empty the directory target on every build (remove then
|
|
161
|
+
// recreate), exactly as the file targets are rewritten empty below,
|
|
162
|
+
// so a leftover store file from an earlier incarnation cannot
|
|
163
|
+
// smuggle configuration in (review finding O1).
|
|
164
|
+
const cleared = runStep(`re-emptying the redirect target ${target}`, () => {
|
|
165
|
+
rmSync(target, { recursive: true, force: true });
|
|
166
|
+
mkdirSync(target, { recursive: true });
|
|
167
|
+
});
|
|
168
|
+
if (!cleared.ok) {
|
|
169
|
+
return { ok: false, reason: cleared.reason };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// The file target is written EMPTY on every build, so a leftover
|
|
174
|
+
// from an earlier incarnation cannot smuggle configuration in.
|
|
175
|
+
const refusal = refuseOpenForWrite(target);
|
|
176
|
+
if (refusal !== undefined) {
|
|
177
|
+
return { ok: false, reason: refusal };
|
|
178
|
+
}
|
|
179
|
+
const written = runStep(`staging the empty redirect target ${target}`, () => writeFileSync(target, ""));
|
|
180
|
+
if (!written.ok) {
|
|
181
|
+
return { ok: false, reason: written.reason };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
env[redirection.name] = target;
|
|
185
|
+
}
|
|
186
|
+
// Apple Git reads a prefix system config in addition to GIT_CONFIG_SYSTEM.
|
|
187
|
+
// This fixed kernel value is applied after copying and cannot be inherited.
|
|
188
|
+
env["GIT_CONFIG_NOSYSTEM"] = "1";
|
|
189
|
+
return { ok: true, env };
|
|
190
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet-home layout (kernel plan v1, M1-P2 step 1; blueprint section 3 with
|
|
3
|
+
* the SC-002/SC-003 resolutions). The fleet home is a small git repository:
|
|
4
|
+
* durable content (charter/, decisions/, tasks/, backlog.md, package.json,
|
|
5
|
+
* .gitignore) is tracked; state/, worktrees/, and projects/ are ephemeral
|
|
6
|
+
* and gitignored (plan decision D-4, PR-004).
|
|
7
|
+
*/
|
|
8
|
+
export declare const FLEET_DIRS: readonly ["charter", "decisions", "state", "tasks", "worktrees", "projects"];
|
|
9
|
+
/** Files every fleet home carries at its root. */
|
|
10
|
+
export declare const FLEET_FILES: readonly ["backlog.md", "package.json", ".gitignore"];
|
|
11
|
+
/**
|
|
12
|
+
* Exactly these entries are gitignored: clones under projects/ are
|
|
13
|
+
* recoverable from their remotes, worktrees/ are disposable, state/ holds
|
|
14
|
+
* beacons and locks (SC-002, plan decision D-4, PR-004). Nothing else.
|
|
15
|
+
*/
|
|
16
|
+
export declare const FLEET_IGNORED: readonly ["state/", "worktrees/", "projects/"];
|
|
17
|
+
/**
|
|
18
|
+
* Well-known state file names. The lease lock is built by M1-P3 and the
|
|
19
|
+
* watcher beacon by M1-P5 (convention FM-043); doctor reads both as files
|
|
20
|
+
* only, never probing a process (plan constraint C-2). Task currency, when
|
|
21
|
+
* a later phase needs it, comes exclusively from tasks/<id>/meta.json and
|
|
22
|
+
* the turn-end file, never from a log tail (plan constraint C-1).
|
|
23
|
+
*/
|
|
24
|
+
export declare const LOCK_FILE: string;
|
|
25
|
+
export declare const BEACON_FILE: string;
|
|
26
|
+
/** Typed accessors over a validated fleet home. */
|
|
27
|
+
export interface Fleet {
|
|
28
|
+
root: string;
|
|
29
|
+
charterDir: string;
|
|
30
|
+
decisionsDir: string;
|
|
31
|
+
stateDir: string;
|
|
32
|
+
tasksDir: string;
|
|
33
|
+
worktreesDir: string;
|
|
34
|
+
projectsDir: string;
|
|
35
|
+
backlogPath: string;
|
|
36
|
+
packageJsonPath: string;
|
|
37
|
+
gitignorePath: string;
|
|
38
|
+
lockPath: string;
|
|
39
|
+
beaconPath: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Return the layout entries missing from dir, in declaration order.
|
|
43
|
+
* Directories are reported with a trailing slash. An empty result means
|
|
44
|
+
* the layout is complete.
|
|
45
|
+
*/
|
|
46
|
+
export declare function missingLayoutEntries(dir: string): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Validate the layout at dir and return typed accessors. Throws an Error
|
|
49
|
+
* naming every missing entry when the layout is incomplete.
|
|
50
|
+
*/
|
|
51
|
+
export declare function loadFleet(dir: string): Fleet;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Fleet-home layout (kernel plan v1, M1-P2 step 1; blueprint section 3 with
|
|
5
|
+
* the SC-002/SC-003 resolutions). The fleet home is a small git repository:
|
|
6
|
+
* durable content (charter/, decisions/, tasks/, backlog.md, package.json,
|
|
7
|
+
* .gitignore) is tracked; state/, worktrees/, and projects/ are ephemeral
|
|
8
|
+
* and gitignored (plan decision D-4, PR-004).
|
|
9
|
+
*/
|
|
10
|
+
export const FLEET_DIRS = [
|
|
11
|
+
"charter",
|
|
12
|
+
"decisions",
|
|
13
|
+
"state",
|
|
14
|
+
"tasks",
|
|
15
|
+
"worktrees",
|
|
16
|
+
"projects",
|
|
17
|
+
];
|
|
18
|
+
/** Files every fleet home carries at its root. */
|
|
19
|
+
export const FLEET_FILES = ["backlog.md", "package.json", ".gitignore"];
|
|
20
|
+
/**
|
|
21
|
+
* Exactly these entries are gitignored: clones under projects/ are
|
|
22
|
+
* recoverable from their remotes, worktrees/ are disposable, state/ holds
|
|
23
|
+
* beacons and locks (SC-002, plan decision D-4, PR-004). Nothing else.
|
|
24
|
+
*/
|
|
25
|
+
export const FLEET_IGNORED = ["state/", "worktrees/", "projects/"];
|
|
26
|
+
/**
|
|
27
|
+
* Well-known state file names. The lease lock is built by M1-P3 and the
|
|
28
|
+
* watcher beacon by M1-P5 (convention FM-043); doctor reads both as files
|
|
29
|
+
* only, never probing a process (plan constraint C-2). Task currency, when
|
|
30
|
+
* a later phase needs it, comes exclusively from tasks/<id>/meta.json and
|
|
31
|
+
* the turn-end file, never from a log tail (plan constraint C-1).
|
|
32
|
+
*/
|
|
33
|
+
export const LOCK_FILE = join("state", "orchestrator.lock");
|
|
34
|
+
export const BEACON_FILE = join("state", "watcher.beacon");
|
|
35
|
+
/**
|
|
36
|
+
* Return the layout entries missing from dir, in declaration order.
|
|
37
|
+
* Directories are reported with a trailing slash. An empty result means
|
|
38
|
+
* the layout is complete.
|
|
39
|
+
*/
|
|
40
|
+
export function missingLayoutEntries(dir) {
|
|
41
|
+
const missing = [];
|
|
42
|
+
for (const name of FLEET_DIRS) {
|
|
43
|
+
const p = join(dir, name);
|
|
44
|
+
if (!existsSync(p) || !statSync(p).isDirectory()) {
|
|
45
|
+
missing.push(`${name}/`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const name of FLEET_FILES) {
|
|
49
|
+
const p = join(dir, name);
|
|
50
|
+
if (!existsSync(p) || !statSync(p).isFile()) {
|
|
51
|
+
missing.push(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return missing;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate the layout at dir and return typed accessors. Throws an Error
|
|
58
|
+
* naming every missing entry when the layout is incomplete.
|
|
59
|
+
*/
|
|
60
|
+
export function loadFleet(dir) {
|
|
61
|
+
const root = resolve(dir);
|
|
62
|
+
const missing = missingLayoutEntries(root);
|
|
63
|
+
if (missing.length > 0) {
|
|
64
|
+
throw new Error(`not a fleet home: ${root} is missing ${missing.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
root,
|
|
68
|
+
charterDir: join(root, "charter"),
|
|
69
|
+
decisionsDir: join(root, "decisions"),
|
|
70
|
+
stateDir: join(root, "state"),
|
|
71
|
+
tasksDir: join(root, "tasks"),
|
|
72
|
+
worktreesDir: join(root, "worktrees"),
|
|
73
|
+
projectsDir: join(root, "projects"),
|
|
74
|
+
backlogPath: join(root, "backlog.md"),
|
|
75
|
+
packageJsonPath: join(root, "package.json"),
|
|
76
|
+
gitignorePath: join(root, ".gitignore"),
|
|
77
|
+
lockPath: join(root, LOCK_FILE),
|
|
78
|
+
beaconPath: join(root, BEACON_FILE),
|
|
79
|
+
};
|
|
80
|
+
}
|