@agent-delivery-harness/action 0.1.0 → 0.5.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/LICENSE +104 -201
- package/NOTICE +1 -1
- package/package.json +9 -4
- package/src/bootstrap.mjs +4 -0
- package/src/loader.mjs +8 -0
- package/src/main.ts +71 -53
- package/src/observation.ts +4 -0
package/LICENSE
CHANGED
|
@@ -1,202 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
the Derivative Works; and
|
|
106
|
-
|
|
107
|
-
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
-
distribution, then any Derivative Works that You distribute must
|
|
109
|
-
include a readable copy of the attribution notices contained
|
|
110
|
-
within such NOTICE file, excluding those notices that do not
|
|
111
|
-
pertain to any part of the Derivative Works, in at least one
|
|
112
|
-
of the following places: within a NOTICE text file distributed
|
|
113
|
-
as part of the Derivative Works; within the Source form or
|
|
114
|
-
documentation, if provided along with the Derivative Works; or,
|
|
115
|
-
within a display generated by the Derivative Works, if and
|
|
116
|
-
wherever such third-party notices normally appear. The contents
|
|
117
|
-
of the NOTICE file are for informational purposes only and
|
|
118
|
-
do not modify the License. You may add Your own attribution
|
|
119
|
-
notices within Derivative Works that You distribute, alongside
|
|
120
|
-
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
-
that such additional attribution notices cannot be construed
|
|
122
|
-
as modifying the License.
|
|
123
|
-
|
|
124
|
-
You may add Your own copyright statement to Your modifications and
|
|
125
|
-
may provide additional or different license terms and conditions
|
|
126
|
-
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
-
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
-
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
-
the conditions stated in this License.
|
|
130
|
-
|
|
131
|
-
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
-
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
-
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
-
this License, without any additional terms or conditions.
|
|
135
|
-
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
-
the terms of any separate license agreement you may have executed
|
|
137
|
-
with Licensor regarding such Contributions.
|
|
138
|
-
|
|
139
|
-
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
-
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
-
except as required for reasonable and customary use in describing the
|
|
142
|
-
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
-
|
|
144
|
-
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
-
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
-
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
-
implied, including, without limitation, any warranties or conditions
|
|
149
|
-
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
-
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
-
appropriateness of using or redistributing the Work and assume any
|
|
152
|
-
risks associated with Your exercise of permissions under this License.
|
|
153
|
-
|
|
154
|
-
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
-
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
-
unless required by applicable law (such as deliberate and grossly
|
|
157
|
-
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
-
liable to You for damages, including any direct, indirect, special,
|
|
159
|
-
incidental, or consequential damages of any character arising as a
|
|
160
|
-
result of this License or out of the use or inability to use the
|
|
161
|
-
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
-
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
-
other commercial damages or losses), even if such Contributor
|
|
164
|
-
has been advised of the possibility of such damages.
|
|
165
|
-
|
|
166
|
-
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
-
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
-
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
-
or other liability obligations and/or rights consistent with this
|
|
170
|
-
License. However, in accepting such obligations, You may act only
|
|
171
|
-
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
-
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
-
defend, and hold each Contributor harmless for any liability
|
|
174
|
-
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
-
of your accepting any such warranty or additional liability.
|
|
176
|
-
|
|
177
|
-
END OF TERMS AND CONDITIONS
|
|
178
|
-
|
|
179
|
-
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
-
|
|
181
|
-
To apply the Apache License to your work, attach the following
|
|
182
|
-
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
-
replaced with your own identifying information. (Don't include
|
|
184
|
-
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
-
comment syntax for the file format. We also recommend that a
|
|
186
|
-
file or class name and description of purpose be included on the
|
|
187
|
-
same "printed page" as the copyright notice for easier
|
|
188
|
-
identification within third-party archives.
|
|
189
|
-
|
|
190
|
-
Copyright [yyyy] [name of copyright owner]
|
|
191
|
-
|
|
192
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
-
you may not use this file except in compliance with the License.
|
|
194
|
-
You may obtain a copy of the License at
|
|
195
|
-
|
|
196
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
-
|
|
198
|
-
Unless required by applicable law or agreed to in writing, software
|
|
199
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
-
See the License for the specific language governing permissions and
|
|
202
|
-
limitations under the License.
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Kwamina Essuah Mensah
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/NOTICE
CHANGED
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-delivery-harness/action",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "GitHub Action verifying a tracked delivery record on pull requests",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "FSL-1.1-ALv2",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/kwam1na/agent-delivery-harness.git",
|
|
10
|
+
"directory": "packages/action"
|
|
11
|
+
},
|
|
7
12
|
"engines": {
|
|
8
|
-
"node": ">=22"
|
|
13
|
+
"node": ">=22.6.0"
|
|
9
14
|
},
|
|
10
15
|
"dependencies": {
|
|
11
|
-
"@agent-delivery-harness/kernel": "0.
|
|
16
|
+
"@agent-delivery-harness/kernel": "0.5.0"
|
|
12
17
|
},
|
|
13
18
|
"exports": {
|
|
14
19
|
".": "./src/index.ts"
|
package/src/loader.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
const kernel = pathToFileURL(realpathSync(fileURLToPath(new URL("../../kernel/src/index.ts", import.meta.url)))).href;
|
|
4
|
+
|
|
5
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
6
|
+
if (specifier === "@agent-delivery-harness/kernel") return { url: kernel, shortCircuit: true };
|
|
7
|
+
return nextResolve(specifier, context);
|
|
8
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
* never consulted for identity. `GITHUB_SHA` is recorded in the summary as the
|
|
40
40
|
* commit that was *not* verified, and is resolved nowhere.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Record freshness remains identity-based. This module reads no clock; the
|
|
43
|
+
* runtime boundary supplies one instant solely to evaluate the expiry of an
|
|
44
|
+
* owner-declared hosted-check exemption. Evidence age never enters admission.
|
|
45
45
|
*
|
|
46
46
|
* FAIL CLOSED, ALWAYS. Every path that cannot reach a verdict — an event that is
|
|
47
47
|
* not a pull request, a payload without a head, a config that will not load, a
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
* skip: a check that reports success because it could not run is worse than no
|
|
51
51
|
* check, because it looks like one.
|
|
52
52
|
*/
|
|
53
|
-
import { realpathSync } from "node:fs";
|
|
54
53
|
import { access, appendFile, readFile } from "node:fs/promises";
|
|
55
54
|
import path from "node:path";
|
|
56
|
-
import {
|
|
55
|
+
import { pathToFileURL } from "node:url";
|
|
56
|
+
import { observeHostedCheckTime } from "./observation.ts";
|
|
57
57
|
import {
|
|
58
58
|
ATTESTATION_LABEL,
|
|
59
59
|
BlockedError,
|
|
@@ -64,15 +64,23 @@ import {
|
|
|
64
64
|
createBlocker,
|
|
65
65
|
createInternalErrorBlocker,
|
|
66
66
|
deliveryRecordPathFor,
|
|
67
|
+
needsCommittedSymlinkTarget,
|
|
68
|
+
parseCandidateTreeListing,
|
|
67
69
|
parseDeliveryRecord,
|
|
68
70
|
renderBlockers,
|
|
69
71
|
runGitCommand,
|
|
70
72
|
selectDeliveryRecordForIdentity,
|
|
71
73
|
validateHarnessConfig,
|
|
72
74
|
verifyDeliveryRecord,
|
|
75
|
+
capturePortableVerificationInputs,
|
|
76
|
+
collectLiveProviderResults,
|
|
77
|
+
isObligationActive,
|
|
78
|
+
invokedDirectly,
|
|
79
|
+
type CapturedCandidate,
|
|
73
80
|
type Blocker,
|
|
74
81
|
type BlockerSource,
|
|
75
82
|
type CandidateCommandRunner,
|
|
83
|
+
type CandidateTreeEntry,
|
|
76
84
|
type DeliveryRecordCheck,
|
|
77
85
|
type DeliveryRecordClaim,
|
|
78
86
|
type DeliveryRecordFile,
|
|
@@ -138,10 +146,13 @@ export const CI_POLICY_INPUT_ENV = "DELIVERY_HARNESS_CI_POLICY_ID";
|
|
|
138
146
|
* simulated git would let the head-vs-merge-ref proof pass against a fiction.
|
|
139
147
|
*/
|
|
140
148
|
export interface ActionRuntime {
|
|
149
|
+
/** Boundary-observed UTC instant used only for owner-declared exemption expiry. */
|
|
150
|
+
readonly observedAt?: string;
|
|
141
151
|
readonly env: EnvSnapshot;
|
|
142
152
|
/** The checked-out repository root. Used as git's cwd, never as the identity source. */
|
|
143
153
|
readonly workspace: string;
|
|
144
154
|
readonly git: CandidateCommandRunner;
|
|
155
|
+
readonly signal?: AbortSignal;
|
|
145
156
|
readonly readFile: (absolutePath: string) => Promise<string>;
|
|
146
157
|
readonly loadConfig: (rootDir: string) => Promise<HarnessConfig>;
|
|
147
158
|
/** Emits the check summary. In a runner this appends to `$GITHUB_STEP_SUMMARY`. */
|
|
@@ -170,9 +181,6 @@ export interface ActionResult {
|
|
|
170
181
|
|
|
171
182
|
// ── Blockers ─────────────────────────────────────────────────────────────────
|
|
172
183
|
|
|
173
|
-
/** `git ls-tree -z` separates records with NUL, so a path containing a newline stays one record. */
|
|
174
|
-
const NUL = "\u0000";
|
|
175
|
-
|
|
176
184
|
const ACTION_SOURCE: BlockerSource = { kind: "command", id: "delivery-harness.action" };
|
|
177
185
|
|
|
178
186
|
const RECORD_AND_COMMIT: Remediation = {
|
|
@@ -270,6 +278,7 @@ const REFUSED_EVENT = "pull_request_target";
|
|
|
270
278
|
|
|
271
279
|
interface PullRequestEvent {
|
|
272
280
|
readonly headSha: string;
|
|
281
|
+
readonly baseSha: string | null;
|
|
273
282
|
readonly headRef: string | null;
|
|
274
283
|
readonly number: number | null;
|
|
275
284
|
}
|
|
@@ -359,12 +368,14 @@ async function resolvePullRequestEvent(runtime: ActionRuntime): Promise<EventRes
|
|
|
359
368
|
if (typeof headSha !== "string" || !HEAD_SHA_GRAMMAR.test(headSha)) {
|
|
360
369
|
return unreadable(`the payload carries no usable pull_request.head.sha (${JSON.stringify(headSha ?? null)})`);
|
|
361
370
|
}
|
|
371
|
+
const baseSha = readMember(readMember(pullRequest, "base"), "sha");
|
|
362
372
|
const headRef = readMember(readMember(pullRequest, "head"), "ref");
|
|
363
373
|
const number = readMember(pullRequest, "number");
|
|
364
374
|
return {
|
|
365
375
|
ok: true,
|
|
366
376
|
event: {
|
|
367
377
|
headSha,
|
|
378
|
+
baseSha: typeof baseSha === "string" && HEAD_SHA_GRAMMAR.test(baseSha) ? baseSha : null,
|
|
368
379
|
headRef: typeof headRef === "string" ? headRef : null,
|
|
369
380
|
number: typeof number === "number" && Number.isSafeInteger(number) ? number : null,
|
|
370
381
|
},
|
|
@@ -476,6 +487,13 @@ interface DiscoveredRecords {
|
|
|
476
487
|
/** Parse failures. Findings, never skips — see below. */
|
|
477
488
|
readonly blockers: readonly Blocker[];
|
|
478
489
|
readonly trackedCount: number;
|
|
490
|
+
/**
|
|
491
|
+
* Every tracked entry in the head tree, for the protected-path check —
|
|
492
|
+
* mode and, for a delivery-owned symlink, the target read from its committed
|
|
493
|
+
* blob, because the one admitted exception under `.claude/skills/` turns on
|
|
494
|
+
* both and neither is visible in a name-only listing.
|
|
495
|
+
*/
|
|
496
|
+
readonly allPaths: readonly CandidateTreeEntry[];
|
|
479
497
|
}
|
|
480
498
|
|
|
481
499
|
/**
|
|
@@ -503,11 +521,12 @@ async function readTrackedRecords(git: GitPort, headSha: string, config: Harness
|
|
|
503
521
|
// from a root-level path of that name — a path/content confusion. The two
|
|
504
522
|
// sides of this check must be anchored identically or they are not comparing
|
|
505
523
|
// the same tree.
|
|
506
|
-
const listing = await git(["ls-tree", "-r", "
|
|
524
|
+
const listing = await git(["ls-tree", "-r", "-z", "--full-tree", headSha]);
|
|
507
525
|
if (listing.exitCode !== 0) {
|
|
508
526
|
return {
|
|
509
527
|
files: [],
|
|
510
528
|
trackedCount: 0,
|
|
529
|
+
allPaths: [],
|
|
511
530
|
blockers: [
|
|
512
531
|
actionBlocker({
|
|
513
532
|
code: "tracked_tree_unreadable",
|
|
@@ -525,7 +544,20 @@ async function readTrackedRecords(git: GitPort, headSha: string, config: Harness
|
|
|
525
544
|
};
|
|
526
545
|
}
|
|
527
546
|
|
|
528
|
-
const
|
|
547
|
+
const listed = parseCandidateTreeListing(listing.stdout);
|
|
548
|
+
const allPaths: CandidateTreeEntry[] = [];
|
|
549
|
+
for (const entry of listed) {
|
|
550
|
+
if (!needsCommittedSymlinkTarget(entry)) {
|
|
551
|
+
allPaths.push(entry);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
// Read out of the commit, like every other byte this verifier judges: the
|
|
555
|
+
// link under review is the one in the tree. A blob that will not read
|
|
556
|
+
// leaves the target absent, and an entry with no target cannot be admitted.
|
|
557
|
+
const link = await git(["cat-file", "blob", entry.objectSha]);
|
|
558
|
+
allPaths.push(link.exitCode === 0 ? { ...entry, symlinkTarget: link.stdout } : entry);
|
|
559
|
+
}
|
|
560
|
+
const paths = listed.map((entry) => entry.path).filter((entry) => isRecordPath(entry, shape));
|
|
529
561
|
const files: DeliveryRecordFile[] = [];
|
|
530
562
|
const blockers: Blocker[] = [];
|
|
531
563
|
for (const recordPath of paths.sort()) {
|
|
@@ -548,7 +580,7 @@ async function readTrackedRecords(git: GitPort, headSha: string, config: Harness
|
|
|
548
580
|
}
|
|
549
581
|
files.push({ path: recordPath, record: parsed.record });
|
|
550
582
|
}
|
|
551
|
-
return { files, blockers, trackedCount: paths.length };
|
|
583
|
+
return { files, blockers, trackedCount: paths.length, allPaths };
|
|
552
584
|
}
|
|
553
585
|
|
|
554
586
|
// ── The summary ──────────────────────────────────────────────────────────────
|
|
@@ -642,6 +674,13 @@ function renderSummary(input: SummaryInput, config: HarnessConfig | null): strin
|
|
|
642
674
|
lines.push(`| Attestation | ${ATTESTATION_LABEL} |`);
|
|
643
675
|
lines.push("");
|
|
644
676
|
|
|
677
|
+
if (input.ok && input.check?.hostedChecks.status === "exempted") {
|
|
678
|
+
const body = JSON.stringify(input.check.hostedChecks.exemption, null, 2)
|
|
679
|
+
.replace(/[\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2066-\u2069]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
680
|
+
const fence = fenceFor(body);
|
|
681
|
+
lines.push("### Hosted checks: exempted", "", `${fence}json`, body, fence, "");
|
|
682
|
+
}
|
|
683
|
+
|
|
645
684
|
if (input.check !== null && config !== null && input.check.claims.length > 0) {
|
|
646
685
|
lines.push("### Claims");
|
|
647
686
|
lines.push("");
|
|
@@ -707,6 +746,7 @@ export async function runAction(runtime: ActionRuntime): Promise<ActionResult> {
|
|
|
707
746
|
try {
|
|
708
747
|
await runtime.writeOutputs({
|
|
709
748
|
verified: String(ok),
|
|
749
|
+
"hosted-checks": ok ? (check?.hostedChecks.status ?? "required") : "blocked",
|
|
710
750
|
mode,
|
|
711
751
|
"head-sha": headSha ?? "",
|
|
712
752
|
"record-path": recordPath ?? "",
|
|
@@ -949,8 +989,24 @@ export async function runAction(runtime: ActionRuntime): Promise<ActionResult> {
|
|
|
949
989
|
}
|
|
950
990
|
|
|
951
991
|
recordPath = selected.path;
|
|
952
|
-
|
|
953
|
-
|
|
992
|
+
const candidate: CapturedCandidate = {
|
|
993
|
+
vcs: "git", treeSha, headSha: event.event.headSha, mode: "clean", statusEntries: [], untrackedFiles: [],
|
|
994
|
+
deliverable: { digest: identity.deliverableDigest, identity: identity.identityToken }, base,
|
|
995
|
+
workspaceId: selected.record.workspaceId,
|
|
996
|
+
};
|
|
997
|
+
const inputs = await capturePortableVerificationInputs(repoRoot, config, candidate, selected.record, runtime.git);
|
|
998
|
+
const activationThreshold = config.activationThreshold;
|
|
999
|
+
if (config.obligations.some(obligation => obligation.freshness === "live" &&
|
|
1000
|
+
isObligationActive(obligation.activation, inputs.projection, activationThreshold)) && base.tipSha !== event.event.baseSha) {
|
|
1001
|
+
blockers.push(actionBlocker({code:"live_provider_base_mismatch",summary:"The configured base does not match the pull request event base for live verification.",
|
|
1002
|
+
remediations:[{id:"fetch-the-pull-request-base",kind:"manual_action",summary:"Fetch the pull request head and base, then rerun the check against that exact checkout."}]}));
|
|
1003
|
+
return await settle();
|
|
1004
|
+
}
|
|
1005
|
+
const live = await collectLiveProviderResults({rootDir:repoRoot,config,candidate,projection:inputs.projection,evidenceContext:inputs.evidenceContext,
|
|
1006
|
+
env:runtime.env,run:runtime.git,...(runtime.signal===undefined?{}:{signal:runtime.signal})});
|
|
1007
|
+
check = verifyDeliveryRecord(config, selected.record, identity, base, { candidateTreePaths: discovered.allPaths, ...inputs, ...(runtime.observedAt === undefined ? {} : { observedAt: runtime.observedAt }), liveResults:live.liveResults,
|
|
1008
|
+
executionContext: classifyExecutionContext({ config, env: runtime.env, stdinIsTTY: false, stdoutIsTTY: false }) });
|
|
1009
|
+
if (!check.ok) blockers.push(...live.blockers, ...check.blockers);
|
|
954
1010
|
return await settle();
|
|
955
1011
|
} catch (error) {
|
|
956
1012
|
blockers.push(
|
|
@@ -990,7 +1046,7 @@ async function workspaceHas(runtime: ActionRuntime, repoRoot: string, relativePa
|
|
|
990
1046
|
*
|
|
991
1047
|
* The Action carries its own loader rather than importing the CLI's: the Action
|
|
992
1048
|
* is a wrapper over the *kernel*, and a dependency on the operator CLI would put
|
|
993
|
-
*
|
|
1049
|
+
* eleven commands and an interactive prompt behind a read-only check. The
|
|
994
1050
|
* validation itself is the kernel's single implementation; only the import and
|
|
995
1051
|
* the blocker's source id are local, so the two loaders cannot disagree about
|
|
996
1052
|
* what a valid config is.
|
|
@@ -1024,6 +1080,7 @@ export function defaultRuntime(): ActionRuntime {
|
|
|
1024
1080
|
const outputPath = process.env["GITHUB_OUTPUT"];
|
|
1025
1081
|
return {
|
|
1026
1082
|
env: process.env,
|
|
1083
|
+
observedAt: observeHostedCheckTime(),
|
|
1027
1084
|
workspace,
|
|
1028
1085
|
git: runGitCommand,
|
|
1029
1086
|
readFile: (absolutePath) => readFile(absolutePath, "utf8"),
|
|
@@ -1048,45 +1105,6 @@ export function defaultRuntime(): ActionRuntime {
|
|
|
1048
1105
|
};
|
|
1049
1106
|
}
|
|
1050
1107
|
|
|
1051
|
-
/** The spelling the filesystem can vouch for: the realpath where it can answer, the spelling itself where it cannot. */
|
|
1052
|
-
function canonicalEntryPath(entryPath: string): string {
|
|
1053
|
-
try {
|
|
1054
|
-
return realpathSync(entryPath);
|
|
1055
|
-
} catch {
|
|
1056
|
-
return entryPath;
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
/**
|
|
1061
|
-
* Whether this module is the entry the process was started with.
|
|
1062
|
-
*
|
|
1063
|
-
* argv and `import.meta.url` may spell the same file differently: argv is the
|
|
1064
|
-
* caller's spelling, and Node builds the module URL from the realpath by
|
|
1065
|
-
* default but from the caller's spelling under `--preserve-symlinks-main`. So
|
|
1066
|
-
* each side is canonicalized independently and the canonical forms compared:
|
|
1067
|
-
* a symlinked spelling matches its realpath whenever the link can be read
|
|
1068
|
-
* (`/tmp` → `/private/tmp` on macOS, a runner's action path, a pnpm workspace
|
|
1069
|
-
* link), and equal spellings still match when neither side resolves.
|
|
1070
|
-
*
|
|
1071
|
-
* What is NOT claimed: a symlink the filesystem cannot resolve cannot be seen
|
|
1072
|
-
* through, and the failing-exit-code floor below sits inside this guard, so an
|
|
1073
|
-
* under-match exits 0 in silence — the Action reporting success having
|
|
1074
|
-
* verified nothing. The floor cannot be hoisted above the guard: that would
|
|
1075
|
-
* stamp a failing exit code on every process that merely *imports* this
|
|
1076
|
-
* module. And a non-`file:` module href (a bundled or single-executable
|
|
1077
|
-
* build) never matches — such a build must invoke `main` explicitly.
|
|
1078
|
-
*/
|
|
1079
|
-
export function invokedDirectly(argvEntry: string | undefined, moduleHref: string): boolean {
|
|
1080
|
-
if (argvEntry === undefined) return false;
|
|
1081
|
-
let modulePath: string;
|
|
1082
|
-
try {
|
|
1083
|
-
modulePath = fileURLToPath(moduleHref);
|
|
1084
|
-
} catch {
|
|
1085
|
-
return false;
|
|
1086
|
-
}
|
|
1087
|
-
return canonicalEntryPath(argvEntry) === canonicalEntryPath(modulePath);
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
1108
|
export async function main(): Promise<number> {
|
|
1091
1109
|
const result = await runAction(defaultRuntime());
|
|
1092
1110
|
return result.exitCode;
|