owlrunkit 0.20.0 → 0.21.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/CHANGELOG.md +48 -0
- package/README.md +63 -10
- package/docs/architecture/OWLCODA_RUNKIT_0201_RELEASE_TRUTH_AND_PROFILE_NORMALIZATION_PATCH_DECISION_20260813.md +95 -0
- package/docs/architecture/OWLCODA_RUNKIT_021_TEAM_DELIVERY_PHASE2_PRODUCT_DECISION_20260813.md +249 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +92 -12
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/package.json +1 -1
- package/packages/attest/README.md +7 -1
- package/packages/attest/consumer-surface-v3.json +275 -0
- package/packages/attest/distribution-manifest-v1.json +14 -0
- package/packages/attest/schemas/canonical-candidate-identity-v1.schema.json +112 -0
- package/packages/attest/schemas/foreign-workspace-filesystem-identity-v1.schema.json +32 -0
- package/packages/attest/schemas/quick-verification-receipt-v4.schema.json +226 -0
- package/packages/attest/src/candidate-identity.mjs +454 -0
- package/packages/attest/src/index.mjs +11 -0
- package/packages/attest/src/quick-receipt-contract.mjs +126 -5
- package/packages/attest/src/quick.mjs +43 -8
- package/{registry-release-policy-v1.json → registry-release-policy-v2.json} +8 -9
- package/sbom.spdx.json +4 -4
- package/scripts/runkit-contract/core-contract.mjs +14 -1
- package/scripts/runkit-contract/fleet-skill-plan.mjs +223 -0
- package/scripts/runkit-contract/formal-workflow.mjs +3 -1
- package/scripts/runkit-contract/inspect-presentation.mjs +56 -0
- package/scripts/runkit-contract/install-codex-skill.mjs +1 -0
- package/scripts/runkit-contract/onboarding-bootstrap.mjs +15 -0
- package/scripts/runkit-contract/onboarding-doctor.mjs +201 -7
- package/scripts/runkit-contract/profile-onboarding.mjs +6 -0
- package/scripts/runkit-contract/project-cli-resolver.mjs +9 -1
- package/scripts/runkit-contract/quick-metrics.mjs +9 -5
- package/scripts/runkit-contract/quick-verify.mjs +108 -24
- package/scripts/runkit-contract/registry-adoption-gate.mjs +12 -3
- package/scripts/runkit-contract/registry-adoption.mjs +10 -5
- package/scripts/runkit-contract/runkit-cli.mjs +289 -19
- package/scripts/runkit-contract/schemas/delivery-lifecycle-v1.schema.json +91 -0
- package/scripts/runkit-contract/schemas/external-gate-v1.schema.json +68 -0
- package/scripts/runkit-contract/schemas/fleet-skill-upgrade-plan-v1.schema.json +140 -0
- package/scripts/runkit-contract/schemas/inspect-compact-v1.schema.json +93 -0
- package/scripts/runkit-contract/schemas/team-delivery-status-v2.schema.json +219 -0
- package/scripts/runkit-contract/schemas/team-project-event-v6.schema.json +82 -0
- package/scripts/runkit-contract/schemas/team-project-event-v7.schema.json +80 -0
- package/scripts/runkit-contract/schemas/team-project-status-v4.schema.json +208 -0
- package/scripts/runkit-contract/sync-release-identity.mjs +87 -0
- package/scripts/runkit-contract/team-delivery.mjs +128 -3
- package/scripts/runkit-contract/team-project.mjs +970 -9
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://owlcoda.dev/schemas/runkit/trust/quick-verification-receipt-v4.schema.json",
|
|
4
|
+
"title": "OwlCoda Quick Verification Receipt V4",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion",
|
|
9
|
+
"receiptId",
|
|
10
|
+
"assurance",
|
|
11
|
+
"authorizationGranted",
|
|
12
|
+
"coreIdentity",
|
|
13
|
+
"controllerWorkspaceRoot",
|
|
14
|
+
"workspaceBefore",
|
|
15
|
+
"sourceWorkspaceRoot",
|
|
16
|
+
"exactCommand",
|
|
17
|
+
"executionIsolation",
|
|
18
|
+
"mutationClasses",
|
|
19
|
+
"foreignWorkspace",
|
|
20
|
+
"verificationContext",
|
|
21
|
+
"startedAt",
|
|
22
|
+
"finishedAt",
|
|
23
|
+
"exitResult",
|
|
24
|
+
"outputArtifacts",
|
|
25
|
+
"workspaceAfter",
|
|
26
|
+
"mutationDecision",
|
|
27
|
+
"issueCodes"
|
|
28
|
+
],
|
|
29
|
+
"properties": {
|
|
30
|
+
"schemaVersion": { "const": "OwlCodaQuickVerificationReceiptV4" },
|
|
31
|
+
"receiptId": { "type": "string", "minLength": 1 },
|
|
32
|
+
"assurance": { "const": "captured_verification" },
|
|
33
|
+
"authorizationGranted": { "const": false },
|
|
34
|
+
"coreIdentity": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["contractVersion", "coreVersion", "coreManifestSha256"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"contractVersion": { "type": "string", "minLength": 1 },
|
|
40
|
+
"coreVersion": { "type": "string", "minLength": 1 },
|
|
41
|
+
"coreManifestSha256": { "$ref": "#/$defs/hash" }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"controllerWorkspaceRoot": { "type": "string", "minLength": 1 },
|
|
45
|
+
"workspaceBefore": {
|
|
46
|
+
"$ref": "quick-verification-receipt-v3.schema.json#/$defs/workspaceSnapshot"
|
|
47
|
+
},
|
|
48
|
+
"sourceWorkspaceRoot": { "type": "string", "minLength": 1 },
|
|
49
|
+
"exactCommand": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"additionalProperties": false,
|
|
52
|
+
"required": ["executable", "argv", "cwd"],
|
|
53
|
+
"properties": {
|
|
54
|
+
"executable": { "type": "string", "minLength": 1 },
|
|
55
|
+
"argv": { "type": "array", "items": { "type": "string" } },
|
|
56
|
+
"cwd": { "type": "string", "minLength": 1 }
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"inputArtifacts": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"additionalProperties": false,
|
|
62
|
+
"required": ["stdin"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"stdin": { "$ref": "quick-verification-receipt-v3.schema.json#/$defs/materialArtifact" }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"executionIsolation": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"required": [
|
|
71
|
+
"schemaVersion",
|
|
72
|
+
"mode",
|
|
73
|
+
"consumerRoot",
|
|
74
|
+
"sourceCopy",
|
|
75
|
+
"cacheEnvironmentVariables",
|
|
76
|
+
"dependencyEnvironment",
|
|
77
|
+
"dependencyEnvironmentAfter",
|
|
78
|
+
"consumerDelta",
|
|
79
|
+
"cleanupStatus"
|
|
80
|
+
],
|
|
81
|
+
"properties": {
|
|
82
|
+
"schemaVersion": { "const": "OwlCodaRunKitQuickIsolationV1" },
|
|
83
|
+
"mode": { "const": "temporary_consumer" },
|
|
84
|
+
"consumerRoot": { "type": "string", "minLength": 1 },
|
|
85
|
+
"sourceCopy": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"additionalProperties": false,
|
|
88
|
+
"required": ["fileCount", "totalBytes", "fingerprint"],
|
|
89
|
+
"properties": {
|
|
90
|
+
"fileCount": { "type": "integer", "minimum": 0 },
|
|
91
|
+
"totalBytes": { "type": "integer", "minimum": 0 },
|
|
92
|
+
"fingerprint": { "$ref": "#/$defs/hash" }
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"cacheEnvironmentVariables": {
|
|
96
|
+
"type": "array",
|
|
97
|
+
"minItems": 1,
|
|
98
|
+
"uniqueItems": true,
|
|
99
|
+
"items": { "type": "string", "minLength": 1 }
|
|
100
|
+
},
|
|
101
|
+
"dependencyEnvironment": {
|
|
102
|
+
"oneOf": [
|
|
103
|
+
{ "type": "null" },
|
|
104
|
+
{ "$ref": "quick-verification-receipt-v3.schema.json#/$defs/dependencyEnvironment" }
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
"dependencyEnvironmentAfter": {
|
|
108
|
+
"oneOf": [
|
|
109
|
+
{ "type": "null" },
|
|
110
|
+
{ "$ref": "quick-verification-receipt-v3.schema.json#/$defs/dependencyEnvironment" }
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
"consumerDelta": {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"additionalProperties": false,
|
|
116
|
+
"required": ["sourcePaths", "ignoredPaths"],
|
|
117
|
+
"properties": {
|
|
118
|
+
"sourcePaths": { "$ref": "quick-verification-receipt-v3.schema.json#/$defs/paths" },
|
|
119
|
+
"ignoredPaths": { "$ref": "quick-verification-receipt-v3.schema.json#/$defs/paths" }
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"cleanupStatus": { "enum": ["removed", "cleanup_failed"] }
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"mutationClasses": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"additionalProperties": false,
|
|
128
|
+
"required": [
|
|
129
|
+
"workspaceSource",
|
|
130
|
+
"boundIgnoredPaths",
|
|
131
|
+
"dependencyEnvironment",
|
|
132
|
+
"consumerSource",
|
|
133
|
+
"consumerIgnoredArtifacts",
|
|
134
|
+
"foreignTarget"
|
|
135
|
+
],
|
|
136
|
+
"properties": {
|
|
137
|
+
"workspaceSource": { "enum": ["unchanged", "changed"] },
|
|
138
|
+
"boundIgnoredPaths": { "enum": ["not_bound", "unchanged", "changed"] },
|
|
139
|
+
"dependencyEnvironment": { "enum": ["not_bound", "unchanged", "changed"] },
|
|
140
|
+
"consumerSource": { "enum": ["unchanged", "changed"] },
|
|
141
|
+
"consumerIgnoredArtifacts": { "enum": ["none", "changed_and_discarded"] },
|
|
142
|
+
"foreignTarget": { "enum": ["unchanged", "changed"] }
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"foreignWorkspace": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"additionalProperties": false,
|
|
148
|
+
"required": [
|
|
149
|
+
"candidateBefore",
|
|
150
|
+
"candidateAfter",
|
|
151
|
+
"filesystemBefore",
|
|
152
|
+
"filesystemAfter",
|
|
153
|
+
"zeroWriteObserved"
|
|
154
|
+
],
|
|
155
|
+
"properties": {
|
|
156
|
+
"candidateBefore": { "$ref": "canonical-candidate-identity-v1.schema.json" },
|
|
157
|
+
"candidateAfter": { "$ref": "canonical-candidate-identity-v1.schema.json" },
|
|
158
|
+
"filesystemBefore": { "$ref": "foreign-workspace-filesystem-identity-v1.schema.json" },
|
|
159
|
+
"filesystemAfter": { "$ref": "foreign-workspace-filesystem-identity-v1.schema.json" },
|
|
160
|
+
"zeroWriteObserved": { "type": "boolean" }
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"verificationContext": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"additionalProperties": false,
|
|
166
|
+
"required": ["platform", "architecture", "runtime"],
|
|
167
|
+
"properties": {
|
|
168
|
+
"platform": { "type": "string", "minLength": 1 },
|
|
169
|
+
"architecture": { "type": "string", "minLength": 1 },
|
|
170
|
+
"runtime": { "type": "string", "minLength": 1 }
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"startedAt": { "type": "string", "format": "date-time" },
|
|
174
|
+
"finishedAt": { "type": "string", "format": "date-time" },
|
|
175
|
+
"exitResult": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"additionalProperties": false,
|
|
178
|
+
"required": ["exitCode", "signal"],
|
|
179
|
+
"properties": {
|
|
180
|
+
"exitCode": { "type": ["integer", "null"] },
|
|
181
|
+
"signal": { "type": ["string", "null"] }
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"outputArtifacts": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"additionalProperties": false,
|
|
187
|
+
"required": ["stdout", "stderr"],
|
|
188
|
+
"properties": {
|
|
189
|
+
"stdout": { "$ref": "quick-verification-receipt-v3.schema.json#/$defs/materialArtifact" },
|
|
190
|
+
"stderr": { "$ref": "quick-verification-receipt-v3.schema.json#/$defs/materialArtifact" }
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"workspaceAfter": {
|
|
194
|
+
"$ref": "quick-verification-receipt-v3.schema.json#/$defs/workspaceSnapshot"
|
|
195
|
+
},
|
|
196
|
+
"mutationDecision": {
|
|
197
|
+
"enum": [
|
|
198
|
+
"source_unchanged",
|
|
199
|
+
"invalidated_by_command_source_mutation",
|
|
200
|
+
"invalidated_by_bound_ignored_mutation",
|
|
201
|
+
"invalidated_by_dependency_environment_mutation",
|
|
202
|
+
"invalidated_by_consumer_source_mutation",
|
|
203
|
+
"invalidated_by_isolation_cleanup_failure",
|
|
204
|
+
"invalidated_by_foreign_workspace_mutation",
|
|
205
|
+
"invalidated_by_multiple_mutations"
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
"issueCodes": {
|
|
209
|
+
"type": "array",
|
|
210
|
+
"uniqueItems": true,
|
|
211
|
+
"items": { "type": "string", "minLength": 1 }
|
|
212
|
+
},
|
|
213
|
+
"signatureRef": {
|
|
214
|
+
"type": "object",
|
|
215
|
+
"additionalProperties": false,
|
|
216
|
+
"required": ["path", "sha256"],
|
|
217
|
+
"properties": {
|
|
218
|
+
"path": { "type": "string", "minLength": 1 },
|
|
219
|
+
"sha256": { "$ref": "#/$defs/hash" }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"$defs": {
|
|
224
|
+
"hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
fstatSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
readlinkSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
canonicalJson,
|
|
21
|
+
hasExactKeys,
|
|
22
|
+
isSha256Ref,
|
|
23
|
+
sha256Bytes,
|
|
24
|
+
sha256Canonical,
|
|
25
|
+
} from "./formal.mjs";
|
|
26
|
+
|
|
27
|
+
export const CANDIDATE_LEDGER_ALGORITHM_V1 = "owlcoda-canonical-candidate-ledger-v1";
|
|
28
|
+
export const FOREIGN_FILESYSTEM_LEDGER_ALGORITHM_V1 = "owlcoda-foreign-workspace-ledger-v1";
|
|
29
|
+
|
|
30
|
+
const MAX_LEDGER_FILES = 100_000;
|
|
31
|
+
const MAX_LEDGER_BYTES = 2 * 1024 * 1024 * 1024;
|
|
32
|
+
const STATUS_ARGV = Object.freeze([
|
|
33
|
+
"git", "-c", "color.ui=false", "-c", "core.abbrev=40",
|
|
34
|
+
"status", "--porcelain=v1", "-z", "--untracked-files=all",
|
|
35
|
+
]);
|
|
36
|
+
const TRANSPORT_DIFF_ARGV = Object.freeze([
|
|
37
|
+
"git", "-c", "color.ui=false", "-c", "core.abbrev=40",
|
|
38
|
+
"diff", "--full-index", "--binary", "--no-ext-diff", "--no-textconv", "HEAD",
|
|
39
|
+
]);
|
|
40
|
+
const GIT_IDENTITY_ENVIRONMENT = Object.freeze({
|
|
41
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
42
|
+
LC_ALL: "C",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function compareCodeUnits(left, right) {
|
|
46
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function git(root, argv) {
|
|
50
|
+
return execFileSync(argv[0], argv.slice(1), {
|
|
51
|
+
cwd: root,
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
54
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
55
|
+
env: {
|
|
56
|
+
...process.env,
|
|
57
|
+
...GIT_IDENTITY_ENVIRONMENT,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sameFileIdentity(left, right) {
|
|
63
|
+
return left.dev === right.dev
|
|
64
|
+
&& left.ino === right.ino
|
|
65
|
+
&& left.mode === right.mode
|
|
66
|
+
&& left.size === right.size
|
|
67
|
+
&& left.mtimeMs === right.mtimeMs
|
|
68
|
+
&& left.ctimeMs === right.ctimeMs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hashRegularFile(absolute, expectedStat, maximumBytes = MAX_LEDGER_BYTES) {
|
|
72
|
+
if (!expectedStat.isFile() || expectedStat.size > maximumBytes) {
|
|
73
|
+
throw new Error(`Candidate file exceeds the bounded ledger limit: ${absolute}`);
|
|
74
|
+
}
|
|
75
|
+
const descriptor = openSync(absolute, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
76
|
+
try {
|
|
77
|
+
const before = fstatSync(descriptor);
|
|
78
|
+
if (!before.isFile() || !sameFileIdentity(before, expectedStat)) {
|
|
79
|
+
throw new Error(`Candidate file identity changed before hashing: ${absolute}`);
|
|
80
|
+
}
|
|
81
|
+
const hash = createHash("sha256");
|
|
82
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
83
|
+
let sizeBytes = 0;
|
|
84
|
+
while (true) {
|
|
85
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.byteLength, null);
|
|
86
|
+
if (bytesRead === 0) break;
|
|
87
|
+
sizeBytes += bytesRead;
|
|
88
|
+
if (sizeBytes > maximumBytes) {
|
|
89
|
+
throw new Error(`Candidate file exceeds the bounded ledger limit: ${absolute}`);
|
|
90
|
+
}
|
|
91
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
92
|
+
}
|
|
93
|
+
const after = fstatSync(descriptor);
|
|
94
|
+
const current = lstatSync(absolute);
|
|
95
|
+
if (!sameFileIdentity(before, after) || !sameFileIdentity(after, current)) {
|
|
96
|
+
throw new Error(`Candidate file changed while hashing: ${absolute}`);
|
|
97
|
+
}
|
|
98
|
+
return { sizeBytes, sha256: `sha256:${hash.digest("hex")}` };
|
|
99
|
+
} finally {
|
|
100
|
+
closeSync(descriptor);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function captureTransportDiff(root) {
|
|
105
|
+
const scratch = realpathSync(mkdtempSync(path.join(tmpdir(), "owlrunkit-candidate-diff-")));
|
|
106
|
+
const output = path.join(scratch, "candidate.diff");
|
|
107
|
+
try {
|
|
108
|
+
const descriptor = openSync(output, "wx", 0o600);
|
|
109
|
+
try {
|
|
110
|
+
execFileSync(TRANSPORT_DIFF_ARGV[0], TRANSPORT_DIFF_ARGV.slice(1), {
|
|
111
|
+
cwd: root,
|
|
112
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
113
|
+
stdio: ["ignore", descriptor, "pipe"],
|
|
114
|
+
env: { ...process.env, ...GIT_IDENTITY_ENVIRONMENT },
|
|
115
|
+
});
|
|
116
|
+
} finally {
|
|
117
|
+
closeSync(descriptor);
|
|
118
|
+
}
|
|
119
|
+
return hashRegularFile(output, lstatSync(output));
|
|
120
|
+
} finally {
|
|
121
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function canonicalGitRoot(requestedRoot) {
|
|
126
|
+
const root = realpathSync(requestedRoot);
|
|
127
|
+
const stat = lstatSync(root);
|
|
128
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
129
|
+
throw new Error("Candidate workspace must be a real directory.");
|
|
130
|
+
}
|
|
131
|
+
const gitRoot = realpathSync(git(root, ["git", "rev-parse", "--show-toplevel"]).trim());
|
|
132
|
+
if (gitRoot !== root) {
|
|
133
|
+
throw new Error("Candidate workspace must be the Git worktree top-level.");
|
|
134
|
+
}
|
|
135
|
+
return root;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeGitPath(value) {
|
|
139
|
+
return value.split(path.sep).join("/");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function safeRelativePath(value) {
|
|
143
|
+
if (typeof value !== "string" || value.length === 0 || path.isAbsolute(value)) return false;
|
|
144
|
+
if (value.includes("\\") || value.includes("\0")) return false;
|
|
145
|
+
return value.split("/").every(segment => segment.length > 0 && segment !== "." && segment !== "..");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function withinRoot(root, candidate) {
|
|
149
|
+
const relative = path.relative(root, candidate);
|
|
150
|
+
return relative === ""
|
|
151
|
+
|| (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function changedRecords(root) {
|
|
155
|
+
const raw = git(root, STATUS_ARGV);
|
|
156
|
+
const fields = raw.split("\0");
|
|
157
|
+
const records = [];
|
|
158
|
+
for (let index = 0; index < fields.length; index += 1) {
|
|
159
|
+
const field = fields[index];
|
|
160
|
+
if (!field) continue;
|
|
161
|
+
const status = field.slice(0, 2);
|
|
162
|
+
const currentPath = normalizeGitPath(field.slice(3));
|
|
163
|
+
let previousPath = null;
|
|
164
|
+
if (status.includes("R") || status.includes("C")) {
|
|
165
|
+
previousPath = normalizeGitPath(fields[index + 1] ?? "");
|
|
166
|
+
index += 1;
|
|
167
|
+
}
|
|
168
|
+
if (!safeRelativePath(currentPath) || previousPath !== null && !safeRelativePath(previousPath)) {
|
|
169
|
+
throw new Error("Git status contains an unsafe candidate path.");
|
|
170
|
+
}
|
|
171
|
+
records.push({ status, path: currentPath, previousPath });
|
|
172
|
+
}
|
|
173
|
+
return records.sort((left, right) => (
|
|
174
|
+
compareCodeUnits(left.path, right.path)
|
|
175
|
+
|| compareCodeUnits(left.previousPath ?? "", right.previousPath ?? "")
|
|
176
|
+
));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function fileLedgerEntry(root, record) {
|
|
180
|
+
const deleted = record.status.includes("D");
|
|
181
|
+
const operation = record.status.includes("R")
|
|
182
|
+
? "renamed"
|
|
183
|
+
: record.status.includes("C")
|
|
184
|
+
? "copied"
|
|
185
|
+
: deleted
|
|
186
|
+
? "deleted"
|
|
187
|
+
: record.status === "??" || record.status.includes("A")
|
|
188
|
+
? "added"
|
|
189
|
+
: "modified";
|
|
190
|
+
if (deleted) {
|
|
191
|
+
return {
|
|
192
|
+
path: record.path,
|
|
193
|
+
previousPath: record.previousPath,
|
|
194
|
+
operation,
|
|
195
|
+
status: record.status,
|
|
196
|
+
kind: null,
|
|
197
|
+
mode: null,
|
|
198
|
+
sizeBytes: null,
|
|
199
|
+
sha256: null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const absolute = path.resolve(root, ...record.path.split("/"));
|
|
203
|
+
if (!withinRoot(root, absolute)) throw new Error(`Candidate path escapes workspace: ${record.path}`);
|
|
204
|
+
const stat = lstatSync(absolute);
|
|
205
|
+
let kind;
|
|
206
|
+
let material;
|
|
207
|
+
if (stat.isSymbolicLink()) {
|
|
208
|
+
kind = "symlink";
|
|
209
|
+
const bytes = Buffer.from(readlinkSync(absolute), "utf8");
|
|
210
|
+
material = { sizeBytes: bytes.byteLength, sha256: sha256Bytes(bytes) };
|
|
211
|
+
} else if (stat.isFile()) {
|
|
212
|
+
kind = "file";
|
|
213
|
+
material = hashRegularFile(absolute, stat);
|
|
214
|
+
} else if (stat.isDirectory()) {
|
|
215
|
+
kind = "gitlink";
|
|
216
|
+
const bytes = Buffer.from(git(absolute, ["git", "rev-parse", "HEAD"]).trim(), "utf8");
|
|
217
|
+
material = { sizeBytes: bytes.byteLength, sha256: sha256Bytes(bytes) };
|
|
218
|
+
} else {
|
|
219
|
+
throw new Error(`Candidate path is not a supported file type: ${record.path}`);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
path: record.path,
|
|
223
|
+
previousPath: record.previousPath,
|
|
224
|
+
operation,
|
|
225
|
+
status: record.status,
|
|
226
|
+
kind,
|
|
227
|
+
mode: stat.mode & 0o777,
|
|
228
|
+
...material,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function ledgerBinding(ledger) {
|
|
233
|
+
const bytes = Buffer.from(canonicalJson(ledger), "utf8");
|
|
234
|
+
return {
|
|
235
|
+
ledgerByteLength: bytes.byteLength,
|
|
236
|
+
ledgerSha256: sha256Bytes(bytes),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function gitVersion() {
|
|
241
|
+
return execFileSync("git", ["--version"], {
|
|
242
|
+
encoding: "utf8",
|
|
243
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
244
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
245
|
+
}).trim();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function captureCanonicalCandidateIdentityV1(workspaceRoot) {
|
|
249
|
+
const root = canonicalGitRoot(workspaceRoot);
|
|
250
|
+
const ledger = [];
|
|
251
|
+
let totalBytes = 0;
|
|
252
|
+
for (const record of changedRecords(root)) {
|
|
253
|
+
const entry = fileLedgerEntry(root, record);
|
|
254
|
+
totalBytes += entry.sizeBytes ?? 0;
|
|
255
|
+
ledger.push(entry);
|
|
256
|
+
if (ledger.length > MAX_LEDGER_FILES || totalBytes > MAX_LEDGER_BYTES) {
|
|
257
|
+
throw new Error("Candidate exceeds the bounded ledger limit.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const binding = ledgerBinding(ledger);
|
|
261
|
+
const headCommit = git(root, ["git", "rev-parse", "HEAD"]).trim();
|
|
262
|
+
const trackedTree = git(root, ["git", "rev-parse", "HEAD^{tree}"]).trim();
|
|
263
|
+
const transport = captureTransportDiff(root);
|
|
264
|
+
const candidateFingerprint = sha256Canonical({
|
|
265
|
+
manifestAlgorithmId: CANDIDATE_LEDGER_ALGORITHM_V1,
|
|
266
|
+
headCommit,
|
|
267
|
+
trackedTree,
|
|
268
|
+
ledgerSha256: binding.ledgerSha256,
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
schemaVersion: "OwlCodaRunKitCanonicalCandidateIdentityV1",
|
|
272
|
+
manifestAlgorithmId: CANDIDATE_LEDGER_ALGORITHM_V1,
|
|
273
|
+
workspaceRoot: root,
|
|
274
|
+
headCommit,
|
|
275
|
+
trackedTree,
|
|
276
|
+
gitVersion: gitVersion(),
|
|
277
|
+
discoveryCommand: [...STATUS_ARGV],
|
|
278
|
+
discoveryEnvironment: { ...GIT_IDENTITY_ENVIRONMENT },
|
|
279
|
+
ledger,
|
|
280
|
+
...binding,
|
|
281
|
+
transportDiff: {
|
|
282
|
+
exactArgv: [...TRANSPORT_DIFF_ARGV],
|
|
283
|
+
environment: { ...GIT_IDENTITY_ENVIRONMENT },
|
|
284
|
+
sizeBytes: transport.sizeBytes,
|
|
285
|
+
sha256: transport.sha256,
|
|
286
|
+
identityRole: "transport_only",
|
|
287
|
+
},
|
|
288
|
+
candidateFingerprint,
|
|
289
|
+
authorizationGranted: false,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function excludedFilesystemPath(relativePath) {
|
|
294
|
+
return relativePath === ".git"
|
|
295
|
+
|| relativePath.startsWith(".git/");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function captureForeignWorkspaceFilesystemIdentityV1(workspaceRoot) {
|
|
299
|
+
const root = canonicalGitRoot(workspaceRoot);
|
|
300
|
+
const ledger = [];
|
|
301
|
+
let totalFileBytes = 0;
|
|
302
|
+
const visit = (directory, prefix) => {
|
|
303
|
+
for (const name of readdirSync(directory).sort(compareCodeUnits)) {
|
|
304
|
+
const relativePath = prefix ? `${prefix}/${name}` : name;
|
|
305
|
+
if (excludedFilesystemPath(relativePath)) continue;
|
|
306
|
+
const absolute = path.join(directory, name);
|
|
307
|
+
const stat = lstatSync(absolute);
|
|
308
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
309
|
+
ledger.push({
|
|
310
|
+
path: relativePath,
|
|
311
|
+
kind: "directory",
|
|
312
|
+
mode: stat.mode & 0o777,
|
|
313
|
+
sizeBytes: 0,
|
|
314
|
+
sha256: null,
|
|
315
|
+
});
|
|
316
|
+
visit(absolute, relativePath);
|
|
317
|
+
} else {
|
|
318
|
+
let kind;
|
|
319
|
+
let material;
|
|
320
|
+
if (stat.isSymbolicLink()) {
|
|
321
|
+
kind = "symlink";
|
|
322
|
+
const bytes = Buffer.from(readlinkSync(absolute), "utf8");
|
|
323
|
+
material = { sizeBytes: bytes.byteLength, sha256: sha256Bytes(bytes) };
|
|
324
|
+
} else if (stat.isFile()) {
|
|
325
|
+
kind = "file";
|
|
326
|
+
if (totalFileBytes + stat.size > MAX_LEDGER_BYTES) {
|
|
327
|
+
throw new Error("Foreign workspace exceeds the bounded zero-write ledger limit.");
|
|
328
|
+
}
|
|
329
|
+
material = hashRegularFile(absolute, stat, MAX_LEDGER_BYTES - totalFileBytes);
|
|
330
|
+
} else {
|
|
331
|
+
throw new Error(`Foreign workspace contains an unsupported file type: ${relativePath}`);
|
|
332
|
+
}
|
|
333
|
+
totalFileBytes += material.sizeBytes;
|
|
334
|
+
ledger.push({
|
|
335
|
+
path: relativePath,
|
|
336
|
+
kind,
|
|
337
|
+
mode: stat.mode & 0o777,
|
|
338
|
+
...material,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (ledger.length > MAX_LEDGER_FILES || totalFileBytes > MAX_LEDGER_BYTES) {
|
|
342
|
+
throw new Error("Foreign workspace exceeds the bounded zero-write ledger limit.");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
visit(root, "");
|
|
347
|
+
ledger.sort((left, right) => compareCodeUnits(left.path, right.path));
|
|
348
|
+
const binding = ledgerBinding(ledger);
|
|
349
|
+
return {
|
|
350
|
+
schemaVersion: "OwlCodaRunKitForeignWorkspaceFilesystemIdentityV1",
|
|
351
|
+
manifestAlgorithmId: FOREIGN_FILESYSTEM_LEDGER_ALGORITHM_V1,
|
|
352
|
+
workspaceRoot: root,
|
|
353
|
+
excludedRoots: [".git"],
|
|
354
|
+
entryCount: ledger.length,
|
|
355
|
+
totalFileBytes,
|
|
356
|
+
...binding,
|
|
357
|
+
filesystemFingerprint: sha256Canonical({
|
|
358
|
+
manifestAlgorithmId: FOREIGN_FILESYSTEM_LEDGER_ALGORITHM_V1,
|
|
359
|
+
ledgerSha256: binding.ledgerSha256,
|
|
360
|
+
entryCount: ledger.length,
|
|
361
|
+
totalFileBytes,
|
|
362
|
+
}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function validLedgerEntry(value) {
|
|
367
|
+
return hasExactKeys(value, [
|
|
368
|
+
"path", "previousPath", "operation", "status", "kind", "mode", "sizeBytes", "sha256",
|
|
369
|
+
])
|
|
370
|
+
&& safeRelativePath(value.path)
|
|
371
|
+
&& (value.previousPath === null || safeRelativePath(value.previousPath))
|
|
372
|
+
&& ["added", "copied", "deleted", "modified", "renamed"].includes(value.operation)
|
|
373
|
+
&& typeof value.status === "string"
|
|
374
|
+
&& value.status.length === 2
|
|
375
|
+
&& (value.kind === null || ["file", "gitlink", "symlink"].includes(value.kind))
|
|
376
|
+
&& (value.mode === null || Number.isInteger(value.mode) && value.mode >= 0 && value.mode <= 0o777)
|
|
377
|
+
&& (value.sizeBytes === null || Number.isInteger(value.sizeBytes) && value.sizeBytes >= 0)
|
|
378
|
+
&& (value.sha256 === null || isSha256Ref(value.sha256));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function validCanonicalCandidateIdentityV1(value) {
|
|
382
|
+
if (!hasExactKeys(value, [
|
|
383
|
+
"schemaVersion", "manifestAlgorithmId", "workspaceRoot", "headCommit", "trackedTree",
|
|
384
|
+
"gitVersion", "discoveryCommand", "discoveryEnvironment", "ledger", "ledgerByteLength", "ledgerSha256",
|
|
385
|
+
"transportDiff", "candidateFingerprint", "authorizationGranted",
|
|
386
|
+
])) return false;
|
|
387
|
+
if (
|
|
388
|
+
value.schemaVersion !== "OwlCodaRunKitCanonicalCandidateIdentityV1"
|
|
389
|
+
|| value.manifestAlgorithmId !== CANDIDATE_LEDGER_ALGORITHM_V1
|
|
390
|
+
|| typeof value.workspaceRoot !== "string"
|
|
391
|
+
|| !path.isAbsolute(value.workspaceRoot)
|
|
392
|
+
|| !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(value.headCommit)
|
|
393
|
+
|| !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(value.trackedTree)
|
|
394
|
+
|| typeof value.gitVersion !== "string"
|
|
395
|
+
|| JSON.stringify(value.discoveryCommand) !== JSON.stringify(STATUS_ARGV)
|
|
396
|
+
|| !hasExactKeys(value.discoveryEnvironment, ["GIT_OPTIONAL_LOCKS", "LC_ALL"])
|
|
397
|
+
|| value.discoveryEnvironment.GIT_OPTIONAL_LOCKS !== "0"
|
|
398
|
+
|| value.discoveryEnvironment.LC_ALL !== "C"
|
|
399
|
+
|| !Array.isArray(value.ledger)
|
|
400
|
+
|| !value.ledger.every(validLedgerEntry)
|
|
401
|
+
|| !isSha256Ref(value.ledgerSha256)
|
|
402
|
+
|| !isSha256Ref(value.candidateFingerprint)
|
|
403
|
+
|| value.authorizationGranted !== false
|
|
404
|
+
|| !hasExactKeys(value.transportDiff, ["exactArgv", "environment", "sizeBytes", "sha256", "identityRole"])
|
|
405
|
+
|| JSON.stringify(value.transportDiff.exactArgv) !== JSON.stringify(TRANSPORT_DIFF_ARGV)
|
|
406
|
+
|| !hasExactKeys(value.transportDiff.environment, ["GIT_OPTIONAL_LOCKS", "LC_ALL"])
|
|
407
|
+
|| value.transportDiff.environment.GIT_OPTIONAL_LOCKS !== "0"
|
|
408
|
+
|| value.transportDiff.environment.LC_ALL !== "C"
|
|
409
|
+
|| !Number.isInteger(value.transportDiff.sizeBytes)
|
|
410
|
+
|| value.transportDiff.sizeBytes < 0
|
|
411
|
+
|| !isSha256Ref(value.transportDiff.sha256)
|
|
412
|
+
|| value.transportDiff.identityRole !== "transport_only"
|
|
413
|
+
) return false;
|
|
414
|
+
const sorted = [...value.ledger].sort((left, right) => (
|
|
415
|
+
compareCodeUnits(left.path, right.path)
|
|
416
|
+
|| compareCodeUnits(left.previousPath ?? "", right.previousPath ?? "")
|
|
417
|
+
));
|
|
418
|
+
const binding = ledgerBinding(value.ledger);
|
|
419
|
+
return canonicalJson(sorted) === canonicalJson(value.ledger)
|
|
420
|
+
&& binding.ledgerByteLength === value.ledgerByteLength
|
|
421
|
+
&& binding.ledgerSha256 === value.ledgerSha256
|
|
422
|
+
&& value.candidateFingerprint === sha256Canonical({
|
|
423
|
+
manifestAlgorithmId: CANDIDATE_LEDGER_ALGORITHM_V1,
|
|
424
|
+
headCommit: value.headCommit,
|
|
425
|
+
trackedTree: value.trackedTree,
|
|
426
|
+
ledgerSha256: value.ledgerSha256,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function validForeignWorkspaceFilesystemIdentityV1(value) {
|
|
431
|
+
return hasExactKeys(value, [
|
|
432
|
+
"schemaVersion", "manifestAlgorithmId", "workspaceRoot", "excludedRoots", "entryCount", "totalFileBytes",
|
|
433
|
+
"ledgerByteLength", "ledgerSha256", "filesystemFingerprint",
|
|
434
|
+
])
|
|
435
|
+
&& value.schemaVersion === "OwlCodaRunKitForeignWorkspaceFilesystemIdentityV1"
|
|
436
|
+
&& value.manifestAlgorithmId === FOREIGN_FILESYSTEM_LEDGER_ALGORITHM_V1
|
|
437
|
+
&& typeof value.workspaceRoot === "string"
|
|
438
|
+
&& path.isAbsolute(value.workspaceRoot)
|
|
439
|
+
&& JSON.stringify(value.excludedRoots) === JSON.stringify([".git"])
|
|
440
|
+
&& Number.isInteger(value.entryCount)
|
|
441
|
+
&& value.entryCount >= 0
|
|
442
|
+
&& Number.isInteger(value.totalFileBytes)
|
|
443
|
+
&& value.totalFileBytes >= 0
|
|
444
|
+
&& Number.isInteger(value.ledgerByteLength)
|
|
445
|
+
&& value.ledgerByteLength >= 0
|
|
446
|
+
&& isSha256Ref(value.ledgerSha256)
|
|
447
|
+
&& isSha256Ref(value.filesystemFingerprint)
|
|
448
|
+
&& value.filesystemFingerprint === sha256Canonical({
|
|
449
|
+
manifestAlgorithmId: FOREIGN_FILESYSTEM_LEDGER_ALGORITHM_V1,
|
|
450
|
+
ledgerSha256: value.ledgerSha256,
|
|
451
|
+
entryCount: value.entryCount,
|
|
452
|
+
totalFileBytes: value.totalFileBytes,
|
|
453
|
+
});
|
|
454
|
+
}
|