halfcycle 0.3.23 → 0.3.25
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/.claude-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +1003 -245
- package/dist/account-credential.d.ts +12 -12
- package/dist/account-credential.d.ts.map +1 -1
- package/dist/banner-facts.d.ts +9 -0
- package/dist/banner-facts.d.ts.map +1 -1
- package/dist/bin.d.ts +2 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +619 -197
- package/dist/bin.js.map +3 -3
- package/dist/build-record/template.d.ts +11 -0
- package/dist/build-record/template.d.ts.map +1 -1
- package/dist/ci-bind.d.ts +116 -0
- package/dist/ci-bind.d.ts.map +1 -0
- package/dist/cli-contract.d.ts +11 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/confirm-identity.d.ts +50 -0
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/create-engagement.d.ts +15 -4
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/device-signin.d.ts +13 -0
- package/dist/device-signin.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +64 -85
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.js +370 -126
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +14 -11
- package/dist/install.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +22 -7
- package/dist/merge-settings.d.ts.map +1 -1
- package/dist/setup/manifest.d.ts.map +1 -1
- package/package.json +1 -1
package/bin/bin.bundle.mjs
CHANGED
|
@@ -14,75 +14,6 @@ function resolveGuardOrigin(env) {
|
|
|
14
14
|
return { origin: DEFAULT_GUARD_ORIGIN, source: "default" };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// dist/config.js
|
|
18
|
-
var EXCERPT_DEFAULTS = {
|
|
19
|
-
maxCharsPerExcerpt: 200,
|
|
20
|
-
maxExcerptsPerFile: 5,
|
|
21
|
-
maxExcerptsPerChangeSet: 20
|
|
22
|
-
};
|
|
23
|
-
function readConfig() {
|
|
24
|
-
const guardServiceUrl = resolveGuardOrigin(process.env).origin;
|
|
25
|
-
const guardServiceToken = process.env["GUARD_SERVICE_TOKEN"];
|
|
26
|
-
const guardEngagementId = process.env["GUARD_ENGAGEMENT_ID"];
|
|
27
|
-
const missing = [];
|
|
28
|
-
if (!guardServiceToken)
|
|
29
|
-
missing.push("GUARD_SERVICE_TOKEN");
|
|
30
|
-
if (!guardEngagementId)
|
|
31
|
-
missing.push("GUARD_ENGAGEMENT_ID");
|
|
32
|
-
if (missing.length > 0)
|
|
33
|
-
return { config: null, missing };
|
|
34
|
-
return {
|
|
35
|
-
config: {
|
|
36
|
-
guardServiceUrl,
|
|
37
|
-
guardServiceToken,
|
|
38
|
-
guardEngagementId
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function readNeverConfiguredReason() {
|
|
43
|
-
return process.env["HALFCYCLE_NOT_THIS_ACCOUNT"] === "1" ? "not-this-account" : "no-credential-yet";
|
|
44
|
-
}
|
|
45
|
-
function readExcerptConfig() {
|
|
46
|
-
const raw = process.env["HALFCYCLE_SOURCE_EXCERPTS"];
|
|
47
|
-
const enabled = (raw ?? "").trim().toLowerCase() === "true";
|
|
48
|
-
const readCap = (name, fallback) => {
|
|
49
|
-
const v = process.env[name];
|
|
50
|
-
if (v === void 0)
|
|
51
|
-
return fallback;
|
|
52
|
-
const n = Number.parseInt(v, 10);
|
|
53
|
-
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
54
|
-
};
|
|
55
|
-
return {
|
|
56
|
-
enabled,
|
|
57
|
-
maxCharsPerExcerpt: readCap("HALFCYCLE_EXCERPT_MAX_CHARS", EXCERPT_DEFAULTS.maxCharsPerExcerpt),
|
|
58
|
-
maxExcerptsPerFile: readCap("HALFCYCLE_EXCERPT_MAX_PER_FILE", EXCERPT_DEFAULTS.maxExcerptsPerFile),
|
|
59
|
-
maxExcerptsPerChangeSet: readCap("HALFCYCLE_EXCERPT_MAX_PER_CHANGESET", EXCERPT_DEFAULTS.maxExcerptsPerChangeSet)
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
function readDiffBaseConfig() {
|
|
63
|
-
const raw = process.env["HALFCYCLE_DIFF_BASE"];
|
|
64
|
-
const trimmed = (raw ?? "").trim();
|
|
65
|
-
if (!trimmed)
|
|
66
|
-
return void 0;
|
|
67
|
-
if (/^0+$/.test(trimmed))
|
|
68
|
-
return void 0;
|
|
69
|
-
return trimmed;
|
|
70
|
-
}
|
|
71
|
-
function readTelemetryConfig() {
|
|
72
|
-
return {
|
|
73
|
-
controlTelemetryUrl: process.env["CONTROL_TELEMETRY_URL"],
|
|
74
|
-
// Reuse GUARD_SERVICE_TOKEN as the per-engagement bearer token for telemetry.
|
|
75
|
-
// The control plane validates the same token on POST /telemetry (Phase 1).
|
|
76
|
-
controlTelemetryToken: process.env["GUARD_SERVICE_TOKEN"],
|
|
77
|
-
// Per-engagement log dir injected by the guard-runner.sh wrapper (E1-T-02).
|
|
78
|
-
// Absent → emit.ts defaults to ~/.halfcycle/guard-eval-log/.
|
|
79
|
-
guardEvalLogDir: process.env["HALFCYCLE_GUARD_EVAL_LOG_DIR"],
|
|
80
|
-
// Phase source injected by the guard-runner.sh wrapper (E1-T-03, #61 seam).
|
|
81
|
-
// Absent → the run is unphased.
|
|
82
|
-
phase: process.env["HALFCYCLE_PHASE"]
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
17
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
87
18
|
var external_exports = {};
|
|
88
19
|
__export(external_exports, {
|
|
@@ -14597,6 +14528,220 @@ function date4(params) {
|
|
|
14597
14528
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
14598
14529
|
config(en_default());
|
|
14599
14530
|
|
|
14531
|
+
// ../core/dist/guard-record.js
|
|
14532
|
+
var severitySchema = external_exports.enum(["info", "warn", "block"]);
|
|
14533
|
+
var channelSchema = external_exports.enum(["stable", "candidate", "rented"]);
|
|
14534
|
+
var matcherKindSchema = external_exports.enum(["grep", "ast", "integrationTest", "llm"]);
|
|
14535
|
+
var matcherConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
|
|
14536
|
+
var grepMatcherSchema = external_exports.object({ kind: external_exports.literal("grep"), config: matcherConfigSchema }).strict();
|
|
14537
|
+
var astMatcherSchema = external_exports.object({ kind: external_exports.literal("ast"), config: matcherConfigSchema }).strict();
|
|
14538
|
+
var integrationTestMatcherSchema = external_exports.object({ kind: external_exports.literal("integrationTest"), config: matcherConfigSchema }).strict();
|
|
14539
|
+
var llmMatcherSchema = external_exports.object({ kind: external_exports.literal("llm"), config: matcherConfigSchema }).strict();
|
|
14540
|
+
var matcherSchema = external_exports.discriminatedUnion("kind", [
|
|
14541
|
+
grepMatcherSchema,
|
|
14542
|
+
astMatcherSchema,
|
|
14543
|
+
integrationTestMatcherSchema,
|
|
14544
|
+
llmMatcherSchema
|
|
14545
|
+
]);
|
|
14546
|
+
|
|
14547
|
+
// ../core/dist/credential-format.js
|
|
14548
|
+
var HALFCYCLE_DIR_NAME = ".halfcycle";
|
|
14549
|
+
var ENGAGEMENTS_DIR_NAME = "engagements";
|
|
14550
|
+
var ENGAGEMENT_ENV_FILENAME = "env";
|
|
14551
|
+
var ACCOUNT_STORE_FILENAME = "account.json";
|
|
14552
|
+
var ENGAGEMENT_ENV_HEADER = "# Halfcycle per-engagement credential \u2014 machine level, owner-only, never in a repository.";
|
|
14553
|
+
function shq(value) {
|
|
14554
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14555
|
+
}
|
|
14556
|
+
function unquote(value) {
|
|
14557
|
+
const v = value.trim();
|
|
14558
|
+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
14559
|
+
return v.slice(1, -1).split(`'\\''`).join(`'`);
|
|
14560
|
+
}
|
|
14561
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
|
|
14562
|
+
return v.slice(1, -1);
|
|
14563
|
+
return v;
|
|
14564
|
+
}
|
|
14565
|
+
function parseEnvText(raw) {
|
|
14566
|
+
const out = {};
|
|
14567
|
+
for (const line of raw.split("\n")) {
|
|
14568
|
+
const eq = line.indexOf("=");
|
|
14569
|
+
if (eq === -1)
|
|
14570
|
+
continue;
|
|
14571
|
+
const key = line.slice(0, eq).replace(/^\s*export\s+/, "").trim();
|
|
14572
|
+
if (key === "" || key.startsWith("#"))
|
|
14573
|
+
continue;
|
|
14574
|
+
out[key] = unquote(line.slice(eq + 1));
|
|
14575
|
+
}
|
|
14576
|
+
return out;
|
|
14577
|
+
}
|
|
14578
|
+
function reconcileEnvText(existing, values, keys) {
|
|
14579
|
+
const desired = new Map(keys.map((k) => {
|
|
14580
|
+
const value = values[k];
|
|
14581
|
+
return [k, value === void 0 ? "" : value];
|
|
14582
|
+
}));
|
|
14583
|
+
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
14584
|
+
const written = keys.filter((k) => desired.get(k) !== null);
|
|
14585
|
+
if (existing === null) {
|
|
14586
|
+
if (written.length === 0)
|
|
14587
|
+
return "";
|
|
14588
|
+
return `${ENGAGEMENT_ENV_HEADER}
|
|
14589
|
+
` + written.map(line).join("\n") + "\n";
|
|
14590
|
+
}
|
|
14591
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14592
|
+
const out = [];
|
|
14593
|
+
for (const existingLine of existing.split("\n")) {
|
|
14594
|
+
const eq = existingLine.indexOf("=");
|
|
14595
|
+
if (eq === -1) {
|
|
14596
|
+
out.push(existingLine);
|
|
14597
|
+
continue;
|
|
14598
|
+
}
|
|
14599
|
+
const lhs = existingLine.slice(0, eq);
|
|
14600
|
+
const exported = /^\s*export\s+/.exec(lhs);
|
|
14601
|
+
const prefix = exported === null ? "" : exported[0];
|
|
14602
|
+
const key = lhs.slice(prefix.length).trim();
|
|
14603
|
+
if (!desired.has(key)) {
|
|
14604
|
+
out.push(existingLine);
|
|
14605
|
+
continue;
|
|
14606
|
+
}
|
|
14607
|
+
seen.add(key);
|
|
14608
|
+
if (desired.get(key) === null)
|
|
14609
|
+
continue;
|
|
14610
|
+
out.push(`${prefix}${line(key)}`);
|
|
14611
|
+
}
|
|
14612
|
+
const missing = written.filter((k) => !seen.has(k));
|
|
14613
|
+
if (missing.length > 0) {
|
|
14614
|
+
const header = existing.includes(ENGAGEMENT_ENV_HEADER) ? "" : `${ENGAGEMENT_ENV_HEADER}
|
|
14615
|
+
`;
|
|
14616
|
+
const block = header + missing.map(line).join("\n");
|
|
14617
|
+
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
14618
|
+
if (trailingBlank)
|
|
14619
|
+
out.splice(out.length - 1, 0, block);
|
|
14620
|
+
else
|
|
14621
|
+
out.push(`
|
|
14622
|
+
${block}`);
|
|
14623
|
+
}
|
|
14624
|
+
let result = out.join("\n");
|
|
14625
|
+
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
14626
|
+
result += "\n";
|
|
14627
|
+
return result;
|
|
14628
|
+
}
|
|
14629
|
+
function normaliseOrigin(serviceUrl) {
|
|
14630
|
+
return serviceUrl.trim().replace(/\/+$/, "").toLowerCase();
|
|
14631
|
+
}
|
|
14632
|
+
function parseAccountStoreText(raw) {
|
|
14633
|
+
try {
|
|
14634
|
+
const parsed = JSON.parse(raw);
|
|
14635
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.accounts !== "object") {
|
|
14636
|
+
return { version: 1, accounts: {} };
|
|
14637
|
+
}
|
|
14638
|
+
return {
|
|
14639
|
+
version: 1,
|
|
14640
|
+
accounts: parsed.accounts ?? {}
|
|
14641
|
+
};
|
|
14642
|
+
} catch {
|
|
14643
|
+
return { version: 1, accounts: {} };
|
|
14644
|
+
}
|
|
14645
|
+
}
|
|
14646
|
+
|
|
14647
|
+
// ../core/dist/ci-audience.js
|
|
14648
|
+
var HALFCYCLE_OIDC_AUDIENCE = "https://halfcycle.ai";
|
|
14649
|
+
|
|
14650
|
+
// dist/control-origin.js
|
|
14651
|
+
var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
|
|
14652
|
+
function resolveControlOrigin(env) {
|
|
14653
|
+
const configured = env["HALFCYCLE_SERVICE_URL"]?.trim();
|
|
14654
|
+
if (configured)
|
|
14655
|
+
return { origin: normaliseOrigin(configured), source: "environment" };
|
|
14656
|
+
return { origin: DEFAULT_CONTROL_ORIGIN, source: "default" };
|
|
14657
|
+
}
|
|
14658
|
+
|
|
14659
|
+
// dist/config.js
|
|
14660
|
+
var EXCERPT_DEFAULTS = {
|
|
14661
|
+
maxCharsPerExcerpt: 200,
|
|
14662
|
+
maxExcerptsPerFile: 5,
|
|
14663
|
+
maxExcerptsPerChangeSet: 20
|
|
14664
|
+
};
|
|
14665
|
+
function readConfig() {
|
|
14666
|
+
const guardServiceUrl = readGuardOrigin().origin;
|
|
14667
|
+
const guardServiceToken = process.env["GUARD_SERVICE_TOKEN"];
|
|
14668
|
+
const guardEngagementId = process.env["GUARD_ENGAGEMENT_ID"];
|
|
14669
|
+
const missing = [];
|
|
14670
|
+
if (!guardServiceToken)
|
|
14671
|
+
missing.push("GUARD_SERVICE_TOKEN");
|
|
14672
|
+
if (!guardEngagementId)
|
|
14673
|
+
missing.push("GUARD_ENGAGEMENT_ID");
|
|
14674
|
+
if (missing.length > 0)
|
|
14675
|
+
return { config: null, missing };
|
|
14676
|
+
return {
|
|
14677
|
+
config: {
|
|
14678
|
+
guardServiceUrl,
|
|
14679
|
+
guardServiceToken,
|
|
14680
|
+
guardEngagementId
|
|
14681
|
+
}
|
|
14682
|
+
};
|
|
14683
|
+
}
|
|
14684
|
+
function readNeverConfiguredReason() {
|
|
14685
|
+
return process.env["HALFCYCLE_NOT_THIS_ACCOUNT"] === "1" ? "not-this-account" : "no-credential-yet";
|
|
14686
|
+
}
|
|
14687
|
+
function readGuardOrigin() {
|
|
14688
|
+
return resolveGuardOrigin(process.env);
|
|
14689
|
+
}
|
|
14690
|
+
function readActionsOidcEnv() {
|
|
14691
|
+
const requestUrl = process.env["ACTIONS_ID_TOKEN_REQUEST_URL"]?.trim();
|
|
14692
|
+
const requestToken = process.env["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]?.trim();
|
|
14693
|
+
if (!requestUrl || !requestToken)
|
|
14694
|
+
return void 0;
|
|
14695
|
+
return { requestUrl, requestToken };
|
|
14696
|
+
}
|
|
14697
|
+
function readControlOrigin() {
|
|
14698
|
+
return resolveControlOrigin(process.env);
|
|
14699
|
+
}
|
|
14700
|
+
function readEnvFilePath() {
|
|
14701
|
+
const raw = process.env["HALFCYCLE_ENV_FILE"]?.trim();
|
|
14702
|
+
return raw ? raw : void 0;
|
|
14703
|
+
}
|
|
14704
|
+
function readExcerptConfig() {
|
|
14705
|
+
const raw = process.env["HALFCYCLE_SOURCE_EXCERPTS"];
|
|
14706
|
+
const enabled = (raw ?? "").trim().toLowerCase() === "true";
|
|
14707
|
+
const readCap = (name, fallback) => {
|
|
14708
|
+
const v = process.env[name];
|
|
14709
|
+
if (v === void 0)
|
|
14710
|
+
return fallback;
|
|
14711
|
+
const n = Number.parseInt(v, 10);
|
|
14712
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
14713
|
+
};
|
|
14714
|
+
return {
|
|
14715
|
+
enabled,
|
|
14716
|
+
maxCharsPerExcerpt: readCap("HALFCYCLE_EXCERPT_MAX_CHARS", EXCERPT_DEFAULTS.maxCharsPerExcerpt),
|
|
14717
|
+
maxExcerptsPerFile: readCap("HALFCYCLE_EXCERPT_MAX_PER_FILE", EXCERPT_DEFAULTS.maxExcerptsPerFile),
|
|
14718
|
+
maxExcerptsPerChangeSet: readCap("HALFCYCLE_EXCERPT_MAX_PER_CHANGESET", EXCERPT_DEFAULTS.maxExcerptsPerChangeSet)
|
|
14719
|
+
};
|
|
14720
|
+
}
|
|
14721
|
+
function readDiffBaseConfig() {
|
|
14722
|
+
const raw = process.env["HALFCYCLE_DIFF_BASE"];
|
|
14723
|
+
const trimmed = (raw ?? "").trim();
|
|
14724
|
+
if (!trimmed)
|
|
14725
|
+
return void 0;
|
|
14726
|
+
if (/^0+$/.test(trimmed))
|
|
14727
|
+
return void 0;
|
|
14728
|
+
return trimmed;
|
|
14729
|
+
}
|
|
14730
|
+
function readTelemetryConfig() {
|
|
14731
|
+
return {
|
|
14732
|
+
controlTelemetryUrl: process.env["CONTROL_TELEMETRY_URL"],
|
|
14733
|
+
// Reuse GUARD_SERVICE_TOKEN as the per-engagement bearer token for telemetry.
|
|
14734
|
+
// The control plane validates the same token on POST /telemetry (Phase 1).
|
|
14735
|
+
controlTelemetryToken: process.env["GUARD_SERVICE_TOKEN"],
|
|
14736
|
+
// Per-engagement log dir injected by the guard-runner.sh wrapper (E1-T-02).
|
|
14737
|
+
// Absent → emit.ts defaults to ~/.halfcycle/guard-eval-log/.
|
|
14738
|
+
guardEvalLogDir: process.env["HALFCYCLE_GUARD_EVAL_LOG_DIR"],
|
|
14739
|
+
// Phase source injected by the guard-runner.sh wrapper (E1-T-03, #61 seam).
|
|
14740
|
+
// Absent → the run is unphased.
|
|
14741
|
+
phase: process.env["HALFCYCLE_PHASE"]
|
|
14742
|
+
};
|
|
14743
|
+
}
|
|
14744
|
+
|
|
14600
14745
|
// ../events/dist/evaluation.js
|
|
14601
14746
|
var envRefSchema = external_exports.object({
|
|
14602
14747
|
name: external_exports.string(),
|
|
@@ -14671,22 +14816,6 @@ var evaluationRequestSchema = external_exports.object({
|
|
|
14671
14816
|
clientCapabilities: external_exports.array(external_exports.string()).optional()
|
|
14672
14817
|
}).strict();
|
|
14673
14818
|
|
|
14674
|
-
// ../core/dist/guard-record.js
|
|
14675
|
-
var severitySchema = external_exports.enum(["info", "warn", "block"]);
|
|
14676
|
-
var channelSchema = external_exports.enum(["stable", "candidate", "rented"]);
|
|
14677
|
-
var matcherKindSchema = external_exports.enum(["grep", "ast", "integrationTest", "llm"]);
|
|
14678
|
-
var matcherConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
|
|
14679
|
-
var grepMatcherSchema = external_exports.object({ kind: external_exports.literal("grep"), config: matcherConfigSchema }).strict();
|
|
14680
|
-
var astMatcherSchema = external_exports.object({ kind: external_exports.literal("ast"), config: matcherConfigSchema }).strict();
|
|
14681
|
-
var integrationTestMatcherSchema = external_exports.object({ kind: external_exports.literal("integrationTest"), config: matcherConfigSchema }).strict();
|
|
14682
|
-
var llmMatcherSchema = external_exports.object({ kind: external_exports.literal("llm"), config: matcherConfigSchema }).strict();
|
|
14683
|
-
var matcherSchema = external_exports.discriminatedUnion("kind", [
|
|
14684
|
-
grepMatcherSchema,
|
|
14685
|
-
astMatcherSchema,
|
|
14686
|
-
integrationTestMatcherSchema,
|
|
14687
|
-
llmMatcherSchema
|
|
14688
|
-
]);
|
|
14689
|
-
|
|
14690
14819
|
// ../events/dist/result.js
|
|
14691
14820
|
var firedGuardSchema = external_exports.object({
|
|
14692
14821
|
guardId: external_exports.string(),
|
|
@@ -14745,6 +14874,173 @@ var wireErrorSchema = external_exports.object({
|
|
|
14745
14874
|
message: external_exports.string()
|
|
14746
14875
|
}).strict();
|
|
14747
14876
|
|
|
14877
|
+
// ../events/dist/state.js
|
|
14878
|
+
var utcIso8601 = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
14879
|
+
var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
|
|
14880
|
+
var stepIdSchema = external_exports.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
|
|
14881
|
+
var methodVersionSchema = external_exports.string().regex(/^\d+\.\d+\.\d+$/);
|
|
14882
|
+
var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
|
|
14883
|
+
var recordKindSchema = external_exports.enum(["artefact", "step-run", "gate", "intervention"]);
|
|
14884
|
+
var methodLayerSchema = external_exports.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
|
|
14885
|
+
var evidenceKindSchema = external_exports.enum(["anchor", "diff", "repro", "question-answer", "no-op"]);
|
|
14886
|
+
var declarableEvidenceKindSchema = external_exports.enum(["anchor", "diff", "repro", "question-answer"]);
|
|
14887
|
+
var dispositionSchema = external_exports.enum([
|
|
14888
|
+
"prevented",
|
|
14889
|
+
"corrected-in-spec",
|
|
14890
|
+
"corrected-in-scope",
|
|
14891
|
+
"blocked-in-code",
|
|
14892
|
+
"flagged",
|
|
14893
|
+
"auto-fixed",
|
|
14894
|
+
"surfaced",
|
|
14895
|
+
"filed-forward"
|
|
14896
|
+
]);
|
|
14897
|
+
var mechanismSchema = external_exports.enum(["guard", "verdict", "served-step", "method-step"]);
|
|
14898
|
+
var artefactStateSchema = external_exports.enum(["created", "updated"]);
|
|
14899
|
+
var QUALIFIED_OUTCOMES = ["declined", "parked"];
|
|
14900
|
+
var qualifiedOutcomeSchema = external_exports.enum(QUALIFIED_OUTCOMES);
|
|
14901
|
+
function isQualifiedOutcome(outcome) {
|
|
14902
|
+
return QUALIFIED_OUTCOMES.includes(outcome);
|
|
14903
|
+
}
|
|
14904
|
+
var runOutcomeSchema = external_exports.enum([
|
|
14905
|
+
"completed",
|
|
14906
|
+
"attempted-failed",
|
|
14907
|
+
...QUALIFIED_OUTCOMES
|
|
14908
|
+
]);
|
|
14909
|
+
var gateKindSchema = external_exports.enum(["approval", "mechanical"]);
|
|
14910
|
+
var gateVerdictSchema = external_exports.enum(["pass", "fail", ...QUALIFIED_OUTCOMES]);
|
|
14911
|
+
function outcomeReasonIssue(outcome, reason) {
|
|
14912
|
+
if (isQualifiedOutcome(outcome) && reason === void 0) {
|
|
14913
|
+
return { message: `outcomeReason is required when the outcome is '${outcome}'` };
|
|
14914
|
+
}
|
|
14915
|
+
if (!isQualifiedOutcome(outcome) && reason !== void 0) {
|
|
14916
|
+
return {
|
|
14917
|
+
message: `outcomeReason is only present on a qualified outcome (${QUALIFIED_OUTCOMES.join(" | ")})`
|
|
14918
|
+
};
|
|
14919
|
+
}
|
|
14920
|
+
return null;
|
|
14921
|
+
}
|
|
14922
|
+
var anchorEvidenceSchema = external_exports.object({
|
|
14923
|
+
kind: external_exports.literal("anchor"),
|
|
14924
|
+
path: external_exports.string().regex(anchorPathRegex),
|
|
14925
|
+
// Optional 1-based line number within the file at `path`. Present when the
|
|
14926
|
+
// writer knows the exact line; omitted — never `0`, never `null` — when it
|
|
14927
|
+
// does not, because a line number is not always obtainable. Absence is a
|
|
14928
|
+
// valid, permanent state here, not a gap meant to be filled in later.
|
|
14929
|
+
line: external_exports.number().optional()
|
|
14930
|
+
}).strict();
|
|
14931
|
+
var diffEvidenceSchema = external_exports.object({
|
|
14932
|
+
kind: external_exports.literal("diff"),
|
|
14933
|
+
ref: external_exports.string()
|
|
14934
|
+
}).strict();
|
|
14935
|
+
var reproEvidenceSchema = external_exports.object({
|
|
14936
|
+
kind: external_exports.literal("repro"),
|
|
14937
|
+
steps: external_exports.string().max(2e3)
|
|
14938
|
+
}).strict();
|
|
14939
|
+
var questionAnswerEvidenceSchema = external_exports.object({
|
|
14940
|
+
kind: external_exports.literal("question-answer"),
|
|
14941
|
+
question: external_exports.string().max(2e3),
|
|
14942
|
+
before: external_exports.string().max(2e3),
|
|
14943
|
+
after: external_exports.string().max(2e3)
|
|
14944
|
+
}).strict();
|
|
14945
|
+
var noOpEvidenceSchema = external_exports.object({
|
|
14946
|
+
kind: external_exports.literal("no-op"),
|
|
14947
|
+
inspected: external_exports.string().max(2e3),
|
|
14948
|
+
unchangedBecause: external_exports.string().max(2e3)
|
|
14949
|
+
}).strict();
|
|
14950
|
+
var evidenceSchema = external_exports.discriminatedUnion("kind", [
|
|
14951
|
+
anchorEvidenceSchema,
|
|
14952
|
+
diffEvidenceSchema,
|
|
14953
|
+
reproEvidenceSchema,
|
|
14954
|
+
questionAnswerEvidenceSchema,
|
|
14955
|
+
noOpEvidenceSchema
|
|
14956
|
+
]);
|
|
14957
|
+
var stateRecordEnvelopeSchema = external_exports.object({
|
|
14958
|
+
format: external_exports.literal(STATE_RECORD_FORMAT),
|
|
14959
|
+
recordKind: recordKindSchema,
|
|
14960
|
+
recordId: external_exports.string().uuid(),
|
|
14961
|
+
methodVersion: methodVersionSchema,
|
|
14962
|
+
engagementId: external_exports.string(),
|
|
14963
|
+
stepId: stepIdSchema,
|
|
14964
|
+
// Immutable: names when the thing happened, never when a later disposition
|
|
14965
|
+
// moved.
|
|
14966
|
+
occurredAt: utcIso8601,
|
|
14967
|
+
// Equal to `occurredAt` on first write; advances on each re-emit OF A FINDING.
|
|
14968
|
+
// Carried on the shared envelope so a "when did this last change" query is
|
|
14969
|
+
// kind-agnostic. Kept rather than generalised to `updatedAt` because
|
|
14970
|
+
// `disposition` is the only field any re-emit may change (§5.3).
|
|
14971
|
+
dispositionAt: utcIso8601,
|
|
14972
|
+
evidence: evidenceSchema,
|
|
14973
|
+
// Which phase of the project this record belongs to, if any. Present on
|
|
14974
|
+
// records written during phase-scoped work; absent on records that run once
|
|
14975
|
+
// for the whole project rather than per phase. Absence is meaningful on its
|
|
14976
|
+
// own — it is never replaced with a placeholder value, and it is read as
|
|
14977
|
+
// "satisfies any phase", not as "unknown".
|
|
14978
|
+
phase: external_exports.string().optional()
|
|
14979
|
+
});
|
|
14980
|
+
var artefactRefSchema = external_exports.object({
|
|
14981
|
+
name: external_exports.string(),
|
|
14982
|
+
parent: external_exports.string()
|
|
14983
|
+
}).strict();
|
|
14984
|
+
var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
14985
|
+
recordKind: external_exports.literal("artefact"),
|
|
14986
|
+
artefactRef: artefactRefSchema,
|
|
14987
|
+
// Some layer wrote it, by construction — an empty array is not a legitimate
|
|
14988
|
+
// state, so the schema says so.
|
|
14989
|
+
writtenByLayers: external_exports.array(methodLayerSchema).min(1),
|
|
14990
|
+
state: artefactStateSchema
|
|
14991
|
+
}).strict();
|
|
14992
|
+
var stepRunRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
14993
|
+
recordKind: external_exports.literal("step-run"),
|
|
14994
|
+
runOutcome: runOutcomeSchema,
|
|
14995
|
+
failureReason: external_exports.string().optional(),
|
|
14996
|
+
outcomeReason: external_exports.string().max(2e3).optional()
|
|
14997
|
+
}).strict().superRefine((rec, ctx) => {
|
|
14998
|
+
if (rec.runOutcome === "attempted-failed" && rec.failureReason === void 0) {
|
|
14999
|
+
ctx.addIssue({
|
|
15000
|
+
code: external_exports.ZodIssueCode.custom,
|
|
15001
|
+
path: ["failureReason"],
|
|
15002
|
+
message: "failureReason is required when runOutcome is 'attempted-failed'"
|
|
15003
|
+
});
|
|
15004
|
+
}
|
|
15005
|
+
if (rec.runOutcome !== "attempted-failed" && rec.failureReason !== void 0) {
|
|
15006
|
+
ctx.addIssue({
|
|
15007
|
+
code: external_exports.ZodIssueCode.custom,
|
|
15008
|
+
path: ["failureReason"],
|
|
15009
|
+
message: "failureReason is only present when runOutcome is 'attempted-failed'"
|
|
15010
|
+
});
|
|
15011
|
+
}
|
|
15012
|
+
const issue2 = outcomeReasonIssue(rec.runOutcome, rec.outcomeReason);
|
|
15013
|
+
if (issue2) {
|
|
15014
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["outcomeReason"], ...issue2 });
|
|
15015
|
+
}
|
|
15016
|
+
});
|
|
15017
|
+
var gateRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
15018
|
+
recordKind: external_exports.literal("gate"),
|
|
15019
|
+
gateKind: gateKindSchema,
|
|
15020
|
+
verdict: gateVerdictSchema,
|
|
15021
|
+
actor: external_exports.string(),
|
|
15022
|
+
outcomeReason: external_exports.string().max(2e3).optional()
|
|
15023
|
+
}).strict().superRefine((rec, ctx) => {
|
|
15024
|
+
const issue2 = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
|
|
15025
|
+
if (issue2) {
|
|
15026
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["outcomeReason"], ...issue2 });
|
|
15027
|
+
}
|
|
15028
|
+
});
|
|
15029
|
+
var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
15030
|
+
recordKind: external_exports.literal("intervention"),
|
|
15031
|
+
layer: methodLayerSchema,
|
|
15032
|
+
mechanism: mechanismSchema,
|
|
15033
|
+
severity: severitySchema,
|
|
15034
|
+
disposition: dispositionSchema,
|
|
15035
|
+
summary: external_exports.string().max(2e3)
|
|
15036
|
+
}).strict();
|
|
15037
|
+
var stateRecordSchema = external_exports.discriminatedUnion("recordKind", [
|
|
15038
|
+
artefactRecordSchema,
|
|
15039
|
+
stepRunRecordSchema,
|
|
15040
|
+
gateRecordSchema,
|
|
15041
|
+
interventionRecordSchema
|
|
15042
|
+
]);
|
|
15043
|
+
|
|
14748
15044
|
// ../events/dist/phase-identity.js
|
|
14749
15045
|
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
14750
15046
|
function isValidPhaseIdentity(identity) {
|
|
@@ -14801,25 +15097,281 @@ function describeIdentity(identity) {
|
|
|
14801
15097
|
return `(a ${typeof identity})`;
|
|
14802
15098
|
}
|
|
14803
15099
|
|
|
14804
|
-
// dist/
|
|
14805
|
-
var
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
15100
|
+
// ../events/dist/credential-wire.js
|
|
15101
|
+
var engagementCredentialResponseSchema = external_exports.object({
|
|
15102
|
+
engagementId: external_exports.string(),
|
|
15103
|
+
sessionToken: external_exports.string(),
|
|
15104
|
+
mcpUrl: external_exports.string(),
|
|
15105
|
+
guardUrl: external_exports.string(),
|
|
15106
|
+
controlTelemetryUrl: external_exports.string()
|
|
15107
|
+
}).strict();
|
|
15108
|
+
var ciTokenExchangeResponseSchema = external_exports.object({
|
|
15109
|
+
token: external_exports.string(),
|
|
15110
|
+
expiresAt: utcIso8601,
|
|
15111
|
+
engagementId: external_exports.string()
|
|
15112
|
+
}).strict();
|
|
15113
|
+
|
|
15114
|
+
// dist/hook-protocol.js
|
|
15115
|
+
import { writeSync } from "node:fs";
|
|
15116
|
+
function emitBlock(reason) {
|
|
15117
|
+
process.stdout.write(JSON.stringify({ decision: "block", reason }) + "\n");
|
|
15118
|
+
process.exit(0);
|
|
15119
|
+
}
|
|
15120
|
+
function emitWarning(text) {
|
|
15121
|
+
process.stdout.write(text + "\n");
|
|
15122
|
+
}
|
|
15123
|
+
function emitInfraWarning(message) {
|
|
15124
|
+
process.stdout.write(`[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}. Session continues (fail-open).
|
|
15125
|
+
`);
|
|
15126
|
+
}
|
|
15127
|
+
function emitLocalFault(message) {
|
|
15128
|
+
process.stdout.write(`[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).
|
|
15129
|
+
`);
|
|
15130
|
+
}
|
|
15131
|
+
function emitContractFault(message) {
|
|
15132
|
+
process.stdout.write(`[Halfcycle] WARNING: Guard response could not be understood \u2014 ${message}
|
|
15133
|
+
This is not a service outage: the guard service answered. The suspect is this repository's own installed halfcycle runner, which is out of date with what the service now sends. Update it: run "npx halfcycle" in this repository, then retry. Session continues (fail-open).
|
|
15134
|
+
`);
|
|
15135
|
+
}
|
|
15136
|
+
var UNCONFIGURED_EXIT_TOOL_USE = 2;
|
|
15137
|
+
var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
|
|
15138
|
+
function emitNeverConfigured(missing, exitCode, reason) {
|
|
15139
|
+
writeSync(2, neverConfiguredReport(missing, reason));
|
|
15140
|
+
process.exitCode = exitCode;
|
|
15141
|
+
}
|
|
15142
|
+
var HOOK_REMEDY = `Fix it by running "npx halfcycle" in this repository: it may open your browser to sign in first, then writes this engagement's credentials to ~/.halfcycle \u2014 outside the repository \u2014 and the hook loads them itself.`;
|
|
15143
|
+
var PERMISSIONS_BLOCK = "\n permissions:\n contents: read\n id-token: write\n\n";
|
|
15144
|
+
var BOTH_LINES_SENTENCE = "BOTH LINES. Declaring any permission replaces the defaults rather than adding to them, so a block naming only the identity token takes read access away from the checkout step and a private repository stops checking out before this check is reached.";
|
|
15145
|
+
var CI_REMEDY = "This job cannot vouch for anything, because it has no way to prove which project it belongs to. Add this to the workflow \u2014 at the top of the file, or on this job:\n" + PERMISSIONS_BLOCK + `${BOTH_LINES_SENTENCE} Then, once, on your own machine in this project, if you have not already: \`npx halfcycle ci bind <owner>/<repo>\`. That is the whole configuration \u2014 there is no secret to store. The older setup, GUARD_SERVICE_TOKEN and GUARD_ENGAGEMENT_ID supplied as repository secrets, still works unchanged, and a job that has both needs neither line.`;
|
|
15146
|
+
function neverConfiguredMessage(missing, remedy, label = "[Halfcycle]") {
|
|
15147
|
+
return `${label} GUARD NEVER CONFIGURED \u2014 this repository has no guard coverage.
|
|
15148
|
+
No evaluation was attempted, because there is nothing to attempt one with: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} absent.
|
|
15149
|
+
This is not a service outage. It means the guard has never run here \u2014 not on this change, and not on any change before it.
|
|
15150
|
+
${remedy}
|
|
15151
|
+
`;
|
|
15152
|
+
}
|
|
15153
|
+
function notThisAccountReport(label) {
|
|
15154
|
+
return `${label} This project belongs to another Halfcycle account. Halfcycle is Solo today \u2014 one person works on a project at a time \u2014 so no credential exists for this machine and none will be issued. Nothing here needs fixing; this copy simply is not checked. Ask the project's owner for view access, or \u2014 if this account is wrong \u2014 sign in as the one that owns the project and run "npx halfcycle" again.
|
|
15155
|
+
`;
|
|
15156
|
+
}
|
|
15157
|
+
function neverConfiguredReport(missing, reason, label = "[Halfcycle]") {
|
|
15158
|
+
switch (reason) {
|
|
15159
|
+
case "no-credential-yet":
|
|
15160
|
+
return neverConfiguredMessage(missing, HOOK_REMEDY, label);
|
|
15161
|
+
case "not-this-account":
|
|
15162
|
+
return notThisAccountReport(label);
|
|
15163
|
+
}
|
|
15164
|
+
}
|
|
15165
|
+
var CREDENTIAL_REJECTED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
15166
|
+
function classifyFailure(failure) {
|
|
15167
|
+
if (failure.kind === "contract-error") {
|
|
15168
|
+
return {
|
|
15169
|
+
kind: "contract",
|
|
15170
|
+
message: failure.message,
|
|
15171
|
+
label: "contract error",
|
|
15172
|
+
reason: `contract error: ${failure.message}`
|
|
15173
|
+
};
|
|
15174
|
+
}
|
|
15175
|
+
if (failure.kind === "wire-error" && CREDENTIAL_REJECTED_STATUSES.has(failure.statusCode)) {
|
|
15176
|
+
const label = `credential rejected (${failure.statusCode})`;
|
|
15177
|
+
return {
|
|
15178
|
+
kind: "credential",
|
|
15179
|
+
statusCode: failure.statusCode,
|
|
15180
|
+
message: failure.message,
|
|
15181
|
+
label,
|
|
15182
|
+
reason: `${label}: ${failure.message}`
|
|
15183
|
+
};
|
|
15184
|
+
}
|
|
15185
|
+
return {
|
|
15186
|
+
kind: "service",
|
|
15187
|
+
message: failure.message,
|
|
15188
|
+
// Unchanged wording for this class ON PURPOSE — a genuine outage still says
|
|
15189
|
+
// exactly what it said before, and still fails open locally. This task moved
|
|
15190
|
+
// ONE condition out of this branch; it did not redefine the branch.
|
|
15191
|
+
label: "infrastructure failure",
|
|
15192
|
+
reason: `infrastructure failure: ${failure.message}`
|
|
15193
|
+
};
|
|
15194
|
+
}
|
|
15195
|
+
var CREDENTIAL_REJECTED_EXIT_TOOL_USE = UNCONFIGURED_EXIT_TOOL_USE;
|
|
15196
|
+
var CREDENTIAL_REJECTED_EXIT_STOP_FAMILY = UNCONFIGURED_EXIT_STOP_FAMILY;
|
|
15197
|
+
function credentialRejectedMessage(statusCode, serviceMessage, remedy, label = "[Halfcycle]") {
|
|
15198
|
+
const forbidden = statusCode === 403;
|
|
15199
|
+
const statusName = forbidden ? "Forbidden" : "Unauthorized";
|
|
15200
|
+
const cause = forbidden ? `this credential is not authorised for the engagement this repository is sending` : `the credential this repository is sending is not one it accepts`;
|
|
15201
|
+
return `${label} GUARD CREDENTIAL REJECTED \u2014 the guard service answered and refused this credential.
|
|
15202
|
+
No evaluation was performed: the service returned ${statusCode} ${statusName} \u2014 ${serviceMessage}
|
|
15203
|
+
This is not a service outage. The guard service is up and replying; ${cause}, so this change was not checked.
|
|
15204
|
+
${remedy}
|
|
15205
|
+
`;
|
|
15206
|
+
}
|
|
15207
|
+
function emitCredentialRejected(statusCode, serviceMessage, exitCode, remedy = HOOK_REMEDY) {
|
|
15208
|
+
writeSync(2, credentialRejectedMessage(statusCode, serviceMessage, remedy));
|
|
15209
|
+
process.exitCode = exitCode;
|
|
15210
|
+
}
|
|
15211
|
+
function credentialRenewalFailedMessage(remedy, label = "[Halfcycle]") {
|
|
15212
|
+
return `${label} This project's stored credential has expired and could not be renewed automatically, so changes in this session will not be checked.
|
|
15213
|
+
${remedy}
|
|
15214
|
+
`;
|
|
15215
|
+
}
|
|
15216
|
+
function notRunReport(notRun, label = "[Halfcycle]") {
|
|
15217
|
+
if (!notRun || notRun.length === 0)
|
|
15218
|
+
return "";
|
|
15219
|
+
const count = notRun.length;
|
|
15220
|
+
const noun = count === 1 ? "check" : "checks";
|
|
15221
|
+
const lines = notRun.map((entry) => ` [NOT RUN] ${entry.patternRef}: ${entry.reason}`);
|
|
15222
|
+
return [`${label} ${count} ${noun} did not run on this change:`, ...lines].join("\n");
|
|
15223
|
+
}
|
|
15224
|
+
|
|
15225
|
+
// dist/ci-oidc.js
|
|
15226
|
+
var CI_EXCHANGE_PATH = "/auth/ci/exchange";
|
|
15227
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
15228
|
+
function idTokenRequestUrl(requestUrl, audience) {
|
|
15229
|
+
const url2 = new URL(requestUrl);
|
|
15230
|
+
url2.searchParams.set("audience", audience);
|
|
15231
|
+
return url2;
|
|
15232
|
+
}
|
|
15233
|
+
async function fetchWithTimeout(fetchFn, input, init) {
|
|
15234
|
+
const controller = new AbortController();
|
|
15235
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
15236
|
+
try {
|
|
15237
|
+
const response = await fetchFn(input, { ...init, signal: controller.signal });
|
|
15238
|
+
return { ok: true, response };
|
|
15239
|
+
} catch (err) {
|
|
15240
|
+
const timedOut = err instanceof Error && err.name === "AbortError";
|
|
15241
|
+
return {
|
|
15242
|
+
ok: false,
|
|
15243
|
+
message: timedOut ? `no answer after ${REQUEST_TIMEOUT_MS}ms` : err instanceof Error ? err.message : String(err)
|
|
15244
|
+
};
|
|
15245
|
+
} finally {
|
|
15246
|
+
clearTimeout(timer);
|
|
15247
|
+
}
|
|
15248
|
+
}
|
|
15249
|
+
async function requestIdentityToken(oidc, fetchFn = globalThis.fetch) {
|
|
15250
|
+
let url2;
|
|
15251
|
+
try {
|
|
15252
|
+
url2 = idTokenRequestUrl(oidc.requestUrl, HALFCYCLE_OIDC_AUDIENCE);
|
|
15253
|
+
} catch {
|
|
15254
|
+
return { ok: false, message: "the token request address this job was given is not a URL" };
|
|
15255
|
+
}
|
|
15256
|
+
const attempt2 = await fetchWithTimeout(fetchFn, url2.toString(), {
|
|
15257
|
+
method: "GET",
|
|
15258
|
+
headers: { Authorization: `Bearer ${oidc.requestToken}` }
|
|
15259
|
+
});
|
|
15260
|
+
if (!attempt2.ok)
|
|
15261
|
+
return { ok: false, message: attempt2.message };
|
|
15262
|
+
const { response } = attempt2;
|
|
15263
|
+
if (!response.ok) {
|
|
15264
|
+
return { ok: false, message: `the token service answered ${response.status}` };
|
|
15265
|
+
}
|
|
15266
|
+
let body;
|
|
15267
|
+
try {
|
|
15268
|
+
body = await response.json();
|
|
15269
|
+
} catch {
|
|
15270
|
+
return { ok: false, message: "the token service did not answer with JSON" };
|
|
15271
|
+
}
|
|
15272
|
+
const value = typeof body === "object" && body !== null ? body.value : void 0;
|
|
15273
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
15274
|
+
return { ok: false, message: "the token service answered without a token" };
|
|
15275
|
+
}
|
|
15276
|
+
return { ok: true, identityToken: value.trim() };
|
|
15277
|
+
}
|
|
15278
|
+
async function exchangeIdentityToken(controlOrigin, identityToken, fetchFn = globalThis.fetch) {
|
|
15279
|
+
const attempt2 = await fetchWithTimeout(fetchFn, `${controlOrigin}${CI_EXCHANGE_PATH}`, {
|
|
15280
|
+
method: "POST",
|
|
15281
|
+
headers: { Authorization: `Bearer ${identityToken}` }
|
|
15282
|
+
});
|
|
15283
|
+
if (!attempt2.ok)
|
|
15284
|
+
return { ok: false, kind: "service", message: attempt2.message };
|
|
15285
|
+
const { response } = attempt2;
|
|
15286
|
+
let body;
|
|
15287
|
+
try {
|
|
15288
|
+
body = await response.json();
|
|
15289
|
+
} catch {
|
|
15290
|
+
return {
|
|
15291
|
+
ok: false,
|
|
15292
|
+
kind: "contract",
|
|
15293
|
+
message: "the control plane did not answer the exchange with JSON"
|
|
15294
|
+
};
|
|
15295
|
+
}
|
|
15296
|
+
if (!response.ok) {
|
|
15297
|
+
const parsed2 = wireErrorSchema.safeParse(body);
|
|
15298
|
+
if (!parsed2.success) {
|
|
15299
|
+
return {
|
|
15300
|
+
ok: false,
|
|
15301
|
+
kind: "contract",
|
|
15302
|
+
message: `the control plane refused the exchange in an unreadable shape (${response.status})`
|
|
15303
|
+
};
|
|
15304
|
+
}
|
|
15305
|
+
const decided = parsed2.data.statusCode === 401 || parsed2.data.statusCode === 403;
|
|
15306
|
+
return decided ? { ok: false, kind: "refused", statusCode: parsed2.data.statusCode, message: parsed2.data.message } : { ok: false, kind: "service", message: parsed2.data.message };
|
|
15307
|
+
}
|
|
15308
|
+
const parsed = ciTokenExchangeResponseSchema.safeParse(body);
|
|
15309
|
+
if (!parsed.success) {
|
|
15310
|
+
return {
|
|
15311
|
+
ok: false,
|
|
15312
|
+
kind: "contract",
|
|
15313
|
+
message: `the exchange response did not match the declared shape: ${parsed.error.message}`
|
|
15314
|
+
};
|
|
15315
|
+
}
|
|
15316
|
+
return {
|
|
15317
|
+
ok: true,
|
|
15318
|
+
credential: {
|
|
15319
|
+
guardServiceToken: parsed.data.token,
|
|
15320
|
+
guardEngagementId: parsed.data.engagementId,
|
|
15321
|
+
expiresAt: parsed.data.expiresAt
|
|
15322
|
+
}
|
|
15323
|
+
};
|
|
15324
|
+
}
|
|
15325
|
+
async function acquireCredentialByIdentityToken(oidc, controlOrigin, fetchFn = globalThis.fetch) {
|
|
15326
|
+
const minted = await requestIdentityToken(oidc, fetchFn);
|
|
15327
|
+
if (!minted.ok)
|
|
15328
|
+
return { ok: false, kind: "provider", message: minted.message };
|
|
15329
|
+
return exchangeIdentityToken(controlOrigin, minted.identityToken, fetchFn);
|
|
15330
|
+
}
|
|
15331
|
+
function credentialExchangeFailureMessage(failure, label = "[Halfcycle CI]") {
|
|
15332
|
+
switch (failure.kind) {
|
|
15333
|
+
case "provider":
|
|
15334
|
+
return `${label} No CI identity token \u2014 ${failure.message}.
|
|
15335
|
+
No evaluation was attempted. This job authenticates by asking its own runner for a signed identity token, which requires the workflow to grant:
|
|
15336
|
+
` + // The block and the both-lines sentence come from the runner's one wording
|
|
15337
|
+
// home (T-15): this text and the never-configured remedy must never
|
|
15338
|
+
// disagree on what the block is.
|
|
15339
|
+
PERMISSIONS_BLOCK + `${BOTH_LINES_SENTENCE} If the workflow already has a permissions block, add \`id-token: write\` to it and leave the rest alone. Or supply the guard secrets instead.
|
|
15340
|
+
`;
|
|
15341
|
+
case "refused":
|
|
15342
|
+
return `${label} CI authentication refused (${failure.statusCode}): ${failure.message}
|
|
15343
|
+
This is NOT a guard violation and NOT an outage \u2014 the identity token was presented and the answer came back. Nothing was evaluated.
|
|
15344
|
+
`;
|
|
15345
|
+
case "service":
|
|
15346
|
+
return `${label} Infrastructure failure: the CI credential could not be obtained \u2014 ${failure.message}.
|
|
15347
|
+
This is NOT a guard violation. Fix the infrastructure issue and re-run.
|
|
15348
|
+
`;
|
|
15349
|
+
case "contract":
|
|
15350
|
+
return `${label} Infrastructure failure (contract error): ${failure.message}.
|
|
15351
|
+
The service answered; the suspect is this runner being older than it.
|
|
15352
|
+
`;
|
|
15353
|
+
}
|
|
15354
|
+
}
|
|
15355
|
+
|
|
15356
|
+
// dist/client.js
|
|
15357
|
+
var REQUEST_TIMEOUT_MS2 = 1e4;
|
|
15358
|
+
async function evaluate(url2, token, request, fetchFn = globalThis.fetch) {
|
|
15359
|
+
return attempt(
|
|
15360
|
+
url2,
|
|
15361
|
+
token,
|
|
15362
|
+
request,
|
|
15363
|
+
fetchFn,
|
|
15364
|
+
/* retry */
|
|
15365
|
+
true
|
|
15366
|
+
);
|
|
15367
|
+
}
|
|
15368
|
+
async function attempt(url2, token, request, fetchFn, retry) {
|
|
15369
|
+
const controller = new AbortController();
|
|
15370
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
|
|
15371
|
+
let response;
|
|
15372
|
+
try {
|
|
15373
|
+
response = await fetchFn(`${url2}/evaluate`, {
|
|
15374
|
+
method: "POST",
|
|
14823
15375
|
headers: {
|
|
14824
15376
|
"Content-Type": "application/json",
|
|
14825
15377
|
Authorization: `Bearer ${token}`
|
|
@@ -14841,7 +15393,7 @@ async function attempt(url2, token, request, fetchFn, retry) {
|
|
|
14841
15393
|
false
|
|
14842
15394
|
);
|
|
14843
15395
|
}
|
|
14844
|
-
const message = isTimeout ? `Guard service timed out after ${
|
|
15396
|
+
const message = isTimeout ? `Guard service timed out after ${REQUEST_TIMEOUT_MS2}ms` : err instanceof Error ? err.message : String(err);
|
|
14845
15397
|
return { ok: false, kind: "infra-error", message };
|
|
14846
15398
|
}
|
|
14847
15399
|
let body;
|
|
@@ -16170,115 +16722,11 @@ async function emitRunRecord(opts) {
|
|
|
16170
16722
|
});
|
|
16171
16723
|
await emitTelemetry(run, {
|
|
16172
16724
|
controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
|
|
16173
|
-
controlTelemetryToken: telemetryConfig.controlTelemetryToken,
|
|
16725
|
+
controlTelemetryToken: opts.credential ?? telemetryConfig.controlTelemetryToken,
|
|
16174
16726
|
guardEvalLogDir: telemetryConfig.guardEvalLogDir
|
|
16175
16727
|
}, { warn: (msg) => process.stderr.write(msg + "\n") });
|
|
16176
16728
|
}
|
|
16177
16729
|
|
|
16178
|
-
// dist/hook-protocol.js
|
|
16179
|
-
import { writeSync } from "node:fs";
|
|
16180
|
-
function emitBlock(reason) {
|
|
16181
|
-
process.stdout.write(JSON.stringify({ decision: "block", reason }) + "\n");
|
|
16182
|
-
process.exit(0);
|
|
16183
|
-
}
|
|
16184
|
-
function emitWarning(text) {
|
|
16185
|
-
process.stdout.write(text + "\n");
|
|
16186
|
-
}
|
|
16187
|
-
function emitInfraWarning(message) {
|
|
16188
|
-
process.stdout.write(`[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}. Session continues (fail-open).
|
|
16189
|
-
`);
|
|
16190
|
-
}
|
|
16191
|
-
function emitLocalFault(message) {
|
|
16192
|
-
process.stdout.write(`[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).
|
|
16193
|
-
`);
|
|
16194
|
-
}
|
|
16195
|
-
function emitContractFault(message) {
|
|
16196
|
-
process.stdout.write(`[Halfcycle] WARNING: Guard response could not be understood \u2014 ${message}
|
|
16197
|
-
This is not a service outage: the guard service answered. The suspect is this repository's own installed halfcycle runner, which is out of date with what the service now sends. Update it: run "npx halfcycle" in this repository, then retry. Session continues (fail-open).
|
|
16198
|
-
`);
|
|
16199
|
-
}
|
|
16200
|
-
var UNCONFIGURED_EXIT_TOOL_USE = 2;
|
|
16201
|
-
var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
|
|
16202
|
-
function emitNeverConfigured(missing, exitCode, reason) {
|
|
16203
|
-
writeSync(2, neverConfiguredReport(missing, reason));
|
|
16204
|
-
process.exitCode = exitCode;
|
|
16205
|
-
}
|
|
16206
|
-
var HOOK_REMEDY = `Fix it by running "npx halfcycle" in this repository: it may open your browser to sign in first, then writes this engagement's credentials to ~/.halfcycle \u2014 outside the repository \u2014 and the hook loads them itself.`;
|
|
16207
|
-
var CI_REMEDY = "This job cannot vouch for anything; supply the guard secrets to the workflow.";
|
|
16208
|
-
function neverConfiguredMessage(missing, remedy, label = "[Halfcycle]") {
|
|
16209
|
-
return `${label} GUARD NEVER CONFIGURED \u2014 this repository has no guard coverage.
|
|
16210
|
-
No evaluation was attempted, because there is nothing to attempt one with: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} absent.
|
|
16211
|
-
This is not a service outage. It means the guard has never run here \u2014 not on this change, and not on any change before it.
|
|
16212
|
-
${remedy}
|
|
16213
|
-
`;
|
|
16214
|
-
}
|
|
16215
|
-
function notThisAccountReport(label) {
|
|
16216
|
-
return `${label} This project belongs to another Halfcycle account. Halfcycle is Solo today \u2014 one person works on a project at a time \u2014 so no credential exists for this machine and none will be issued. Nothing here needs fixing; this copy simply is not checked. Ask the project's owner for view access, or \u2014 if this account is wrong \u2014 sign in as the one that owns the project and run "npx halfcycle" again.
|
|
16217
|
-
`;
|
|
16218
|
-
}
|
|
16219
|
-
function neverConfiguredReport(missing, reason, label = "[Halfcycle]") {
|
|
16220
|
-
switch (reason) {
|
|
16221
|
-
case "no-credential-yet":
|
|
16222
|
-
return neverConfiguredMessage(missing, HOOK_REMEDY, label);
|
|
16223
|
-
case "not-this-account":
|
|
16224
|
-
return notThisAccountReport(label);
|
|
16225
|
-
}
|
|
16226
|
-
}
|
|
16227
|
-
var CREDENTIAL_REJECTED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
16228
|
-
function classifyFailure(failure) {
|
|
16229
|
-
if (failure.kind === "contract-error") {
|
|
16230
|
-
return {
|
|
16231
|
-
kind: "contract",
|
|
16232
|
-
message: failure.message,
|
|
16233
|
-
label: "contract error",
|
|
16234
|
-
reason: `contract error: ${failure.message}`
|
|
16235
|
-
};
|
|
16236
|
-
}
|
|
16237
|
-
if (failure.kind === "wire-error" && CREDENTIAL_REJECTED_STATUSES.has(failure.statusCode)) {
|
|
16238
|
-
const label = `credential rejected (${failure.statusCode})`;
|
|
16239
|
-
return {
|
|
16240
|
-
kind: "credential",
|
|
16241
|
-
statusCode: failure.statusCode,
|
|
16242
|
-
message: failure.message,
|
|
16243
|
-
label,
|
|
16244
|
-
reason: `${label}: ${failure.message}`
|
|
16245
|
-
};
|
|
16246
|
-
}
|
|
16247
|
-
return {
|
|
16248
|
-
kind: "service",
|
|
16249
|
-
message: failure.message,
|
|
16250
|
-
// Unchanged wording for this class ON PURPOSE — a genuine outage still says
|
|
16251
|
-
// exactly what it said before, and still fails open locally. This task moved
|
|
16252
|
-
// ONE condition out of this branch; it did not redefine the branch.
|
|
16253
|
-
label: "infrastructure failure",
|
|
16254
|
-
reason: `infrastructure failure: ${failure.message}`
|
|
16255
|
-
};
|
|
16256
|
-
}
|
|
16257
|
-
var CREDENTIAL_REJECTED_EXIT_TOOL_USE = UNCONFIGURED_EXIT_TOOL_USE;
|
|
16258
|
-
var CREDENTIAL_REJECTED_EXIT_STOP_FAMILY = UNCONFIGURED_EXIT_STOP_FAMILY;
|
|
16259
|
-
function credentialRejectedMessage(statusCode, serviceMessage, remedy, label = "[Halfcycle]") {
|
|
16260
|
-
const forbidden = statusCode === 403;
|
|
16261
|
-
const statusName = forbidden ? "Forbidden" : "Unauthorized";
|
|
16262
|
-
const cause = forbidden ? `this credential is not authorised for the engagement this repository is sending` : `the credential this repository is sending is not one it accepts`;
|
|
16263
|
-
return `${label} GUARD CREDENTIAL REJECTED \u2014 the guard service answered and refused this credential.
|
|
16264
|
-
No evaluation was performed: the service returned ${statusCode} ${statusName} \u2014 ${serviceMessage}
|
|
16265
|
-
This is not a service outage. The guard service is up and replying; ${cause}, so this change was not checked.
|
|
16266
|
-
${remedy}
|
|
16267
|
-
`;
|
|
16268
|
-
}
|
|
16269
|
-
function emitCredentialRejected(statusCode, serviceMessage, exitCode) {
|
|
16270
|
-
writeSync(2, credentialRejectedMessage(statusCode, serviceMessage, HOOK_REMEDY));
|
|
16271
|
-
process.exitCode = exitCode;
|
|
16272
|
-
}
|
|
16273
|
-
function notRunReport(notRun, label = "[Halfcycle]") {
|
|
16274
|
-
if (!notRun || notRun.length === 0)
|
|
16275
|
-
return "";
|
|
16276
|
-
const count = notRun.length;
|
|
16277
|
-
const noun = count === 1 ? "check" : "checks";
|
|
16278
|
-
const lines = notRun.map((entry) => ` [NOT RUN] ${entry.patternRef}: ${entry.reason}`);
|
|
16279
|
-
return [`${label} ${count} ${noun} did not run on this change:`, ...lines].join("\n");
|
|
16280
|
-
}
|
|
16281
|
-
|
|
16282
16730
|
// dist/ci.js
|
|
16283
16731
|
async function runCi() {
|
|
16284
16732
|
const repoRootResult = getRepoRoot(process.cwd());
|
|
@@ -16320,12 +16768,37 @@ async function runCi() {
|
|
|
16320
16768
|
const context = loadChangeSetContext(repoRoot);
|
|
16321
16769
|
const changeSet = buildChangeSet(fileDiffEntries, { ...context, excerpts: readExcerptConfig() });
|
|
16322
16770
|
const configResult = readConfig();
|
|
16771
|
+
if (!configResult.config) {
|
|
16772
|
+
const oidc = readActionsOidcEnv();
|
|
16773
|
+
if (oidc) {
|
|
16774
|
+
const acquired = await acquireCredentialByIdentityToken(oidc, readControlOrigin().origin);
|
|
16775
|
+
if (!acquired.ok) {
|
|
16776
|
+
await emitRunRecord({
|
|
16777
|
+
runType: "ci",
|
|
16778
|
+
outcome: acquired.kind === "contract" ? "contract-error" : "infra-error",
|
|
16779
|
+
failureReason: `ci credential: ${acquired.kind}: ${acquired.message}`,
|
|
16780
|
+
changeSet
|
|
16781
|
+
});
|
|
16782
|
+
process.stderr.write(credentialExchangeFailureMessage(acquired));
|
|
16783
|
+
return 1;
|
|
16784
|
+
}
|
|
16785
|
+
return await evaluateAndAct({
|
|
16786
|
+
guardServiceUrl: readGuardOrigin().origin,
|
|
16787
|
+
guardServiceToken: acquired.credential.guardServiceToken,
|
|
16788
|
+
guardEngagementId: acquired.credential.guardEngagementId,
|
|
16789
|
+
changeSet
|
|
16790
|
+
});
|
|
16791
|
+
}
|
|
16792
|
+
}
|
|
16323
16793
|
if (!configResult.config) {
|
|
16324
16794
|
await emitRunRecord({ runType: "ci", outcome: "unconfigured", changeSet });
|
|
16325
16795
|
process.stderr.write(neverConfiguredMessage(configResult.missing, CI_REMEDY, "[Halfcycle CI]"));
|
|
16326
16796
|
return 1;
|
|
16327
16797
|
}
|
|
16328
|
-
|
|
16798
|
+
return await evaluateAndAct({ ...configResult.config, changeSet });
|
|
16799
|
+
}
|
|
16800
|
+
async function evaluateAndAct(args2) {
|
|
16801
|
+
const { guardServiceUrl, guardServiceToken, guardEngagementId, changeSet } = args2;
|
|
16329
16802
|
const clientResult = await evaluate(guardServiceUrl, guardServiceToken, {
|
|
16330
16803
|
engagementId: guardEngagementId,
|
|
16331
16804
|
changeSet,
|
|
@@ -16368,7 +16841,23 @@ async function runCi() {
|
|
|
16368
16841
|
});
|
|
16369
16842
|
await emitTelemetry(run, {
|
|
16370
16843
|
controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
|
|
16371
|
-
|
|
16844
|
+
// THE CREDENTIAL THIS RUN EVALUATED WITH — the argument, never a second read of
|
|
16845
|
+
// the environment. `readTelemetryConfig().controlTelemetryToken` is the stored
|
|
16846
|
+
// `GUARD_SERVICE_TOKEN`, and on the stored path it is the SAME `process.env` read
|
|
16847
|
+
// this argument came from, so nothing changes there.
|
|
16848
|
+
//
|
|
16849
|
+
// **IT IS NOT `telemetryConfig.controlTelemetryToken ?? guardServiceToken`, AND
|
|
16850
|
+
// THE DIFFERENCE IS A HALF-MIGRATED WORKFLOW** (R-09 blocker). That `??` is
|
|
16851
|
+
// environment-first, so the stored variable wins wherever it is set — including
|
|
16852
|
+
// on the identity-token path, which is reached when `readConfig()` returned
|
|
16853
|
+
// `null`, i.e. when the pair is PARTIAL as well as when it is absent. A workflow
|
|
16854
|
+
// with `GUARD_ENGAGEMENT_ID` removed and `GUARD_SERVICE_TOKEN` still injected —
|
|
16855
|
+
// one of two workflow files edited, an org secret still mapped, one matrix leg
|
|
16856
|
+
// missed — would evaluate correctly against the engagement the plane resolved and
|
|
16857
|
+
// then POST its run record with the stale token, which `bindEngagement` refuses
|
|
16858
|
+
// 403. Every run record from that workflow is lost, and the only trace names a
|
|
16859
|
+
// status rather than the wrong credential.
|
|
16860
|
+
controlTelemetryToken: guardServiceToken,
|
|
16372
16861
|
// Per-engagement log dir from HALFCYCLE_GUARD_EVAL_LOG_DIR (E1-T-02, #61).
|
|
16373
16862
|
guardEvalLogDir: telemetryConfig.guardEvalLogDir
|
|
16374
16863
|
}, {
|
|
@@ -16413,6 +16902,196 @@ function actOnEnvelope(envelope) {
|
|
|
16413
16902
|
}
|
|
16414
16903
|
}
|
|
16415
16904
|
|
|
16905
|
+
// dist/credential-store.js
|
|
16906
|
+
import { chmodSync, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
|
|
16907
|
+
import { homedir as homedir2, platform } from "node:os";
|
|
16908
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
16909
|
+
var REFRESHED_TOKEN_KEYS = ["HALFCYCLE_TOKEN", "GUARD_SERVICE_TOKEN"];
|
|
16910
|
+
var TOKEN_READ_KEY = "GUARD_SERVICE_TOKEN";
|
|
16911
|
+
var UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16912
|
+
function composeEngagementEnvPath(engagementId, home) {
|
|
16913
|
+
return join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, ENGAGEMENT_ENV_FILENAME);
|
|
16914
|
+
}
|
|
16915
|
+
function readAccountCredential(controlOrigin, home) {
|
|
16916
|
+
let raw;
|
|
16917
|
+
try {
|
|
16918
|
+
raw = readFileSync5(join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ACCOUNT_STORE_FILENAME), "utf-8");
|
|
16919
|
+
} catch {
|
|
16920
|
+
return void 0;
|
|
16921
|
+
}
|
|
16922
|
+
const entry = parseAccountStoreText(raw).accounts[normaliseOrigin(controlOrigin)];
|
|
16923
|
+
if (!entry || typeof entry.credential !== "string" || entry.credential.trim() === "") {
|
|
16924
|
+
return void 0;
|
|
16925
|
+
}
|
|
16926
|
+
return entry.credential;
|
|
16927
|
+
}
|
|
16928
|
+
function readStoredEngagementToken(envFilePath) {
|
|
16929
|
+
let raw;
|
|
16930
|
+
try {
|
|
16931
|
+
raw = readFileSync5(envFilePath, "utf-8");
|
|
16932
|
+
} catch {
|
|
16933
|
+
return void 0;
|
|
16934
|
+
}
|
|
16935
|
+
const value = parseEnvText(raw)[TOKEN_READ_KEY];
|
|
16936
|
+
return value && value.trim() !== "" ? value : void 0;
|
|
16937
|
+
}
|
|
16938
|
+
function writeRefreshedToken(opts) {
|
|
16939
|
+
if (!UUID_SHAPE.test(opts.engagementId)) {
|
|
16940
|
+
return { written: false, reason: "the engagement this session names is not one this store can be keyed by" };
|
|
16941
|
+
}
|
|
16942
|
+
let declaredReal;
|
|
16943
|
+
let composedReal;
|
|
16944
|
+
try {
|
|
16945
|
+
declaredReal = realpathSync(opts.envFilePath);
|
|
16946
|
+
composedReal = realpathSync(composeEngagementEnvPath(opts.engagementId, opts.home));
|
|
16947
|
+
} catch {
|
|
16948
|
+
return { written: false, reason: "the credential file named for this session could not be resolved on disk" };
|
|
16949
|
+
}
|
|
16950
|
+
if (declaredReal !== composedReal) {
|
|
16951
|
+
return {
|
|
16952
|
+
written: false,
|
|
16953
|
+
reason: "the credential file named for this session is not this engagement's own credential file"
|
|
16954
|
+
};
|
|
16955
|
+
}
|
|
16956
|
+
let existing;
|
|
16957
|
+
try {
|
|
16958
|
+
existing = readFileSync5(declaredReal, "utf-8");
|
|
16959
|
+
} catch {
|
|
16960
|
+
existing = null;
|
|
16961
|
+
}
|
|
16962
|
+
const body = reconcileEnvText(existing, { HALFCYCLE_TOKEN: opts.token, GUARD_SERVICE_TOKEN: opts.token }, REFRESHED_TOKEN_KEYS);
|
|
16963
|
+
const temp = join5(dirname3(declaredReal), `.${ENGAGEMENT_ENV_FILENAME}.${process.pid}.tmp`);
|
|
16964
|
+
try {
|
|
16965
|
+
writeFileSync2(temp, body, { mode: 384 });
|
|
16966
|
+
if (platform() !== "win32")
|
|
16967
|
+
chmodSync(temp, 384);
|
|
16968
|
+
renameSync2(temp, declaredReal);
|
|
16969
|
+
} catch (err) {
|
|
16970
|
+
try {
|
|
16971
|
+
unlinkSync2(temp);
|
|
16972
|
+
} catch {
|
|
16973
|
+
}
|
|
16974
|
+
return {
|
|
16975
|
+
written: false,
|
|
16976
|
+
reason: `the credential file could not be replaced: ${err instanceof Error ? err.message : String(err)}`
|
|
16977
|
+
};
|
|
16978
|
+
}
|
|
16979
|
+
return { written: true };
|
|
16980
|
+
}
|
|
16981
|
+
|
|
16982
|
+
// dist/credential-refresh.js
|
|
16983
|
+
var CONTROL_TIMEOUT_MS = 5e3;
|
|
16984
|
+
var UNAUTHORIZED = 401;
|
|
16985
|
+
async function probeAccount(controlOrigin, credential, fetchFn = globalThis.fetch) {
|
|
16986
|
+
const controller = new AbortController();
|
|
16987
|
+
const timer = setTimeout(() => controller.abort(), CONTROL_TIMEOUT_MS);
|
|
16988
|
+
try {
|
|
16989
|
+
const response = await fetchFn(`${controlOrigin}/account`, {
|
|
16990
|
+
method: "GET",
|
|
16991
|
+
headers: { Authorization: `Bearer ${credential}` },
|
|
16992
|
+
signal: controller.signal
|
|
16993
|
+
});
|
|
16994
|
+
return { reached: true, status: response.status };
|
|
16995
|
+
} catch {
|
|
16996
|
+
return { reached: false };
|
|
16997
|
+
} finally {
|
|
16998
|
+
clearTimeout(timer);
|
|
16999
|
+
}
|
|
17000
|
+
}
|
|
17001
|
+
async function mintThroughJoin(controlOrigin, engagementId, accountCredential, fetchFn) {
|
|
17002
|
+
const controller = new AbortController();
|
|
17003
|
+
const timer = setTimeout(() => controller.abort(), CONTROL_TIMEOUT_MS);
|
|
17004
|
+
let response;
|
|
17005
|
+
try {
|
|
17006
|
+
response = await fetchFn(`${controlOrigin}/engagements/${encodeURIComponent(engagementId)}/join`, {
|
|
17007
|
+
method: "POST",
|
|
17008
|
+
headers: {
|
|
17009
|
+
"Content-Type": "application/json",
|
|
17010
|
+
Authorization: `Bearer ${accountCredential}`
|
|
17011
|
+
},
|
|
17012
|
+
body: JSON.stringify({}),
|
|
17013
|
+
signal: controller.signal
|
|
17014
|
+
});
|
|
17015
|
+
} catch {
|
|
17016
|
+
return { ok: false, remedy: UNREACHABLE_REMEDY };
|
|
17017
|
+
} finally {
|
|
17018
|
+
clearTimeout(timer);
|
|
17019
|
+
}
|
|
17020
|
+
let body;
|
|
17021
|
+
try {
|
|
17022
|
+
body = await response.json();
|
|
17023
|
+
} catch {
|
|
17024
|
+
body = void 0;
|
|
17025
|
+
}
|
|
17026
|
+
if (!response.ok) {
|
|
17027
|
+
if (response.status === UNAUTHORIZED)
|
|
17028
|
+
return { ok: false, remedy: HOOK_REMEDY };
|
|
17029
|
+
return { ok: false, remedy: planeSentence(body) ?? UNREADABLE_REFUSAL_REMEDY };
|
|
17030
|
+
}
|
|
17031
|
+
const parsed = engagementCredentialResponseSchema.safeParse(body);
|
|
17032
|
+
if (!parsed.success)
|
|
17033
|
+
return { ok: false, remedy: UNREADABLE_MINT_REMEDY };
|
|
17034
|
+
return { ok: true, token: parsed.data.sessionToken, source: "mint" };
|
|
17035
|
+
}
|
|
17036
|
+
function planeSentence(body) {
|
|
17037
|
+
if (typeof body !== "object" || body === null)
|
|
17038
|
+
return void 0;
|
|
17039
|
+
const message = body.message;
|
|
17040
|
+
return typeof message === "string" && message.trim() !== "" ? message : void 0;
|
|
17041
|
+
}
|
|
17042
|
+
var UNREACHABLE_REMEDY = "The Halfcycle service could not be reached to renew this credential. Nothing is wrong with this repository; the next change will try again.";
|
|
17043
|
+
var UNREADABLE_REFUSAL_REMEDY = "The Halfcycle service declined to renew this credential and gave no reason this version could read.";
|
|
17044
|
+
var UNREADABLE_MINT_REMEDY = 'The Halfcycle service answered with a credential this version could not read. Update the installed Halfcycle runner: run "npx halfcycle" in this repository.';
|
|
17045
|
+
async function refreshEngagementCredential(opts) {
|
|
17046
|
+
const envFilePath = readEnvFilePath();
|
|
17047
|
+
if (!envFilePath)
|
|
17048
|
+
return { ok: false, remedy: HOOK_REMEDY };
|
|
17049
|
+
const stored = readStoredEngagementToken(envFilePath);
|
|
17050
|
+
if (stored !== void 0 && stored !== opts.refusedToken) {
|
|
17051
|
+
return { ok: true, token: stored, source: "disk" };
|
|
17052
|
+
}
|
|
17053
|
+
const controlOrigin = readControlOrigin().origin;
|
|
17054
|
+
const accountCredential = readAccountCredential(controlOrigin, opts.home);
|
|
17055
|
+
if (!accountCredential)
|
|
17056
|
+
return { ok: false, remedy: HOOK_REMEDY };
|
|
17057
|
+
const minted = await mintThroughJoin(controlOrigin, opts.engagementId, accountCredential, opts.fetchFn ?? globalThis.fetch);
|
|
17058
|
+
if (!minted.ok)
|
|
17059
|
+
return minted;
|
|
17060
|
+
const write = writeRefreshedToken({
|
|
17061
|
+
envFilePath,
|
|
17062
|
+
engagementId: opts.engagementId,
|
|
17063
|
+
token: minted.token,
|
|
17064
|
+
home: opts.home
|
|
17065
|
+
});
|
|
17066
|
+
if (!write.written) {
|
|
17067
|
+
const warn = opts.warn ?? ((message) => process.stderr.write(message));
|
|
17068
|
+
warn(`[Halfcycle] This engagement's credential was renewed for this run only \u2014 ${write.reason}.
|
|
17069
|
+
`);
|
|
17070
|
+
}
|
|
17071
|
+
return minted;
|
|
17072
|
+
}
|
|
17073
|
+
async function evaluateWithRefresh(opts) {
|
|
17074
|
+
const fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
17075
|
+
const first = await evaluate(opts.guardServiceUrl, opts.token, opts.request, fetchFn);
|
|
17076
|
+
if (first.ok)
|
|
17077
|
+
return { result: first, token: opts.token, remedy: HOOK_REMEDY };
|
|
17078
|
+
const failure = classifyFailure(first);
|
|
17079
|
+
if (failure.kind !== "credential" || failure.statusCode !== UNAUTHORIZED) {
|
|
17080
|
+
return { result: first, token: opts.token, remedy: HOOK_REMEDY };
|
|
17081
|
+
}
|
|
17082
|
+
const refreshed = await refreshEngagementCredential({
|
|
17083
|
+
engagementId: opts.engagementId,
|
|
17084
|
+
refusedToken: opts.token,
|
|
17085
|
+
fetchFn,
|
|
17086
|
+
home: opts.home,
|
|
17087
|
+
warn: opts.warn
|
|
17088
|
+
});
|
|
17089
|
+
if (!refreshed.ok)
|
|
17090
|
+
return { result: first, token: opts.token, remedy: refreshed.remedy };
|
|
17091
|
+
const retry = await evaluate(opts.guardServiceUrl, refreshed.token, opts.request, fetchFn);
|
|
17092
|
+
return { result: retry, token: refreshed.token, remedy: HOOK_REMEDY };
|
|
17093
|
+
}
|
|
17094
|
+
|
|
16416
17095
|
// dist/hooks/post-tool-use.js
|
|
16417
17096
|
var HANDLED_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
16418
17097
|
async function runPostToolUse() {
|
|
@@ -16451,11 +17130,17 @@ async function runPostToolUse() {
|
|
|
16451
17130
|
return;
|
|
16452
17131
|
}
|
|
16453
17132
|
const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
|
|
16454
|
-
const
|
|
17133
|
+
const evaluation = await evaluateWithRefresh({
|
|
17134
|
+
guardServiceUrl,
|
|
17135
|
+
token: guardServiceToken,
|
|
16455
17136
|
engagementId: guardEngagementId,
|
|
16456
|
-
|
|
16457
|
-
|
|
17137
|
+
request: {
|
|
17138
|
+
engagementId: guardEngagementId,
|
|
17139
|
+
changeSet,
|
|
17140
|
+
clientCapabilities: deriveClientCapabilities(changeSet)
|
|
17141
|
+
}
|
|
16458
17142
|
});
|
|
17143
|
+
const clientResult = evaluation.result;
|
|
16459
17144
|
if (!clientResult.ok) {
|
|
16460
17145
|
const failure = classifyFailure(clientResult);
|
|
16461
17146
|
await emitRunRecord({
|
|
@@ -16463,10 +17148,19 @@ async function runPostToolUse() {
|
|
|
16463
17148
|
runType: "hook",
|
|
16464
17149
|
outcome: outcomeForClientFailure(clientResult.kind),
|
|
16465
17150
|
failureReason: failure.reason,
|
|
16466
|
-
changeSet
|
|
17151
|
+
changeSet,
|
|
17152
|
+
credential: evaluation.token
|
|
16467
17153
|
});
|
|
16468
17154
|
if (failure.kind === "credential") {
|
|
16469
|
-
emitCredentialRejected(
|
|
17155
|
+
emitCredentialRejected(
|
|
17156
|
+
failure.statusCode,
|
|
17157
|
+
failure.message,
|
|
17158
|
+
CREDENTIAL_REJECTED_EXIT_TOOL_USE,
|
|
17159
|
+
// The remedy the renewal attempt earned: the sign-in when the account
|
|
17160
|
+
// credential is the thing that was refused, and otherwise the plane's own
|
|
17161
|
+
// sentence about why it would not mint.
|
|
17162
|
+
evaluation.remedy
|
|
17163
|
+
);
|
|
16470
17164
|
return;
|
|
16471
17165
|
}
|
|
16472
17166
|
if (failure.kind === "contract") {
|
|
@@ -16489,7 +17183,13 @@ async function runPostToolUse() {
|
|
|
16489
17183
|
});
|
|
16490
17184
|
await emitTelemetry(run, {
|
|
16491
17185
|
controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
|
|
16492
|
-
|
|
17186
|
+
// THE CREDENTIAL IN FORCE, NOT THE ONE IN THE ENVIRONMENT (T-08,
|
|
17187
|
+
// credential-lifecycle). `readTelemetryConfig` reads the bearer off
|
|
17188
|
+
// `process.env` after the evaluation, and a renewal does not write there — so
|
|
17189
|
+
// taking its value on a run that just recovered would present the credential
|
|
17190
|
+
// the plane had already refused and drop the record with a 401, silently,
|
|
17191
|
+
// because telemetry is best-effort.
|
|
17192
|
+
controlTelemetryToken: evaluation.token,
|
|
16493
17193
|
// Per-engagement log dir from HALFCYCLE_GUARD_EVAL_LOG_DIR (E1-T-02, #61).
|
|
16494
17194
|
guardEvalLogDir: telemetryConfig.guardEvalLogDir
|
|
16495
17195
|
}, {
|
|
@@ -16528,9 +17228,9 @@ function readStdin() {
|
|
|
16528
17228
|
|
|
16529
17229
|
// dist/hooks/marker.js
|
|
16530
17230
|
import { createHash } from "node:crypto";
|
|
16531
|
-
import { existsSync as existsSync2, readFileSync as
|
|
17231
|
+
import { existsSync as existsSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
|
|
16532
17232
|
import { tmpdir } from "node:os";
|
|
16533
|
-
import { join as
|
|
17233
|
+
import { join as join6 } from "node:path";
|
|
16534
17234
|
var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
16535
17235
|
var MARKER_PREFIX = "halfcycle-session-";
|
|
16536
17236
|
var MARKER_SUFFIX = ".ref";
|
|
@@ -16548,19 +17248,19 @@ function markerFileName(repoRoot, sessionId) {
|
|
|
16548
17248
|
return `${MARKER_PREFIX}${key}${MARKER_SUFFIX}`;
|
|
16549
17249
|
}
|
|
16550
17250
|
function markerPath(repoRoot, sessionId) {
|
|
16551
|
-
return
|
|
17251
|
+
return join6(markerDir(), markerFileName(repoRoot, sessionId));
|
|
16552
17252
|
}
|
|
16553
17253
|
function dedupePath(repoRoot, sessionId) {
|
|
16554
17254
|
const hash2 = repoHash(repoRoot);
|
|
16555
17255
|
const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
|
|
16556
|
-
return
|
|
17256
|
+
return join6(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
|
|
16557
17257
|
}
|
|
16558
17258
|
function readMarker(repoRoot, sessionId) {
|
|
16559
17259
|
const path = markerPath(repoRoot, sessionId);
|
|
16560
17260
|
if (!existsSync2(path))
|
|
16561
17261
|
return null;
|
|
16562
17262
|
try {
|
|
16563
|
-
return
|
|
17263
|
+
return readFileSync6(path, "utf-8").trim() || null;
|
|
16564
17264
|
} catch {
|
|
16565
17265
|
return null;
|
|
16566
17266
|
}
|
|
@@ -16569,7 +17269,7 @@ function deleteMarker(repoRoot, sessionId) {
|
|
|
16569
17269
|
for (const path of [markerPath(repoRoot, sessionId), dedupePath(repoRoot, sessionId)]) {
|
|
16570
17270
|
try {
|
|
16571
17271
|
if (existsSync2(path))
|
|
16572
|
-
|
|
17272
|
+
unlinkSync3(path);
|
|
16573
17273
|
} catch {
|
|
16574
17274
|
}
|
|
16575
17275
|
}
|
|
@@ -16579,7 +17279,7 @@ function readDedupe(repoRoot, sessionId) {
|
|
|
16579
17279
|
if (!existsSync2(path))
|
|
16580
17280
|
return null;
|
|
16581
17281
|
try {
|
|
16582
|
-
const parsed = JSON.parse(
|
|
17282
|
+
const parsed = JSON.parse(readFileSync6(path, "utf-8"));
|
|
16583
17283
|
if (typeof parsed === "object" && parsed !== null && typeof parsed.base === "string" && typeof parsed.digest === "string") {
|
|
16584
17284
|
return parsed;
|
|
16585
17285
|
}
|
|
@@ -16590,7 +17290,7 @@ function readDedupe(repoRoot, sessionId) {
|
|
|
16590
17290
|
}
|
|
16591
17291
|
function writeDedupe(repoRoot, sessionId, state) {
|
|
16592
17292
|
try {
|
|
16593
|
-
|
|
17293
|
+
writeFileSync3(dedupePath(repoRoot, sessionId), JSON.stringify(state), "utf-8");
|
|
16594
17294
|
} catch {
|
|
16595
17295
|
}
|
|
16596
17296
|
}
|
|
@@ -16621,10 +17321,10 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
|
|
|
16621
17321
|
continue;
|
|
16622
17322
|
if (name === keepName)
|
|
16623
17323
|
continue;
|
|
16624
|
-
const full =
|
|
17324
|
+
const full = join6(dir, name);
|
|
16625
17325
|
let unusable = false;
|
|
16626
17326
|
try {
|
|
16627
|
-
const ref =
|
|
17327
|
+
const ref = readFileSync6(full, "utf-8").trim();
|
|
16628
17328
|
if (!ref || !refKnown(ref))
|
|
16629
17329
|
unusable = true;
|
|
16630
17330
|
} catch {
|
|
@@ -16717,11 +17417,17 @@ async function runSessionDiff(input, kind) {
|
|
|
16717
17417
|
return;
|
|
16718
17418
|
}
|
|
16719
17419
|
const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
|
|
16720
|
-
const
|
|
17420
|
+
const evaluation = await evaluateWithRefresh({
|
|
17421
|
+
guardServiceUrl,
|
|
17422
|
+
token: guardServiceToken,
|
|
16721
17423
|
engagementId: guardEngagementId,
|
|
16722
|
-
|
|
16723
|
-
|
|
17424
|
+
request: {
|
|
17425
|
+
engagementId: guardEngagementId,
|
|
17426
|
+
changeSet,
|
|
17427
|
+
clientCapabilities: deriveClientCapabilities(changeSet)
|
|
17428
|
+
}
|
|
16724
17429
|
});
|
|
17430
|
+
const clientResult = evaluation.result;
|
|
16725
17431
|
if (!clientResult.ok) {
|
|
16726
17432
|
const failure = classifyFailure(clientResult);
|
|
16727
17433
|
await emitRunRecord({
|
|
@@ -16730,10 +17436,19 @@ async function runSessionDiff(input, kind) {
|
|
|
16730
17436
|
outcome: outcomeForClientFailure(clientResult.kind),
|
|
16731
17437
|
failureReason: failure.reason,
|
|
16732
17438
|
changeSet,
|
|
16733
|
-
diffBase
|
|
17439
|
+
diffBase,
|
|
17440
|
+
credential: evaluation.token
|
|
16734
17441
|
});
|
|
16735
17442
|
if (failure.kind === "credential") {
|
|
16736
|
-
emitCredentialRejected(
|
|
17443
|
+
emitCredentialRejected(
|
|
17444
|
+
failure.statusCode,
|
|
17445
|
+
failure.message,
|
|
17446
|
+
CREDENTIAL_REJECTED_EXIT_STOP_FAMILY,
|
|
17447
|
+
// The remedy the renewal attempt earned: the sign-in when the account
|
|
17448
|
+
// credential is what was refused, and otherwise the plane's own sentence
|
|
17449
|
+
// about why it would not mint.
|
|
17450
|
+
evaluation.remedy
|
|
17451
|
+
);
|
|
16737
17452
|
return;
|
|
16738
17453
|
}
|
|
16739
17454
|
if (failure.kind === "contract") {
|
|
@@ -16755,7 +17470,12 @@ async function runSessionDiff(input, kind) {
|
|
|
16755
17470
|
});
|
|
16756
17471
|
await emitTelemetry(run, {
|
|
16757
17472
|
controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
|
|
16758
|
-
|
|
17473
|
+
// THE CREDENTIAL IN FORCE, NOT THE ONE IN THE ENVIRONMENT (T-08,
|
|
17474
|
+
// credential-lifecycle). The telemetry bearer is otherwise read off
|
|
17475
|
+
// `process.env` after the evaluation, which a renewal does not write to — so a
|
|
17476
|
+
// run that just recovered would present the refused credential and drop its
|
|
17477
|
+
// record with a 401, silently, because telemetry is best-effort.
|
|
17478
|
+
controlTelemetryToken: evaluation.token,
|
|
16759
17479
|
guardEvalLogDir: telemetryConfig.guardEvalLogDir
|
|
16760
17480
|
}, {
|
|
16761
17481
|
warn: (msg) => process.stderr.write(msg + "\n")
|
|
@@ -16836,17 +17556,17 @@ async function runStop() {
|
|
|
16836
17556
|
}
|
|
16837
17557
|
|
|
16838
17558
|
// dist/resolve-engagement.js
|
|
16839
|
-
import { existsSync as existsSync3, readFileSync as
|
|
16840
|
-
import { join as
|
|
16841
|
-
import { homedir as
|
|
17559
|
+
import { existsSync as existsSync3, readFileSync as readFileSync7, realpathSync as realpathSync2 } from "node:fs";
|
|
17560
|
+
import { join as join7, sep as sep2 } from "node:path";
|
|
17561
|
+
import { homedir as homedir3 } from "node:os";
|
|
16842
17562
|
function defaultRegistryPath() {
|
|
16843
|
-
return
|
|
17563
|
+
return join7(homedir3(), ".halfcycle", "registry.json");
|
|
16844
17564
|
}
|
|
16845
17565
|
function readRegistry(registryPath = defaultRegistryPath()) {
|
|
16846
17566
|
if (!existsSync3(registryPath))
|
|
16847
17567
|
return {};
|
|
16848
17568
|
try {
|
|
16849
|
-
const parsed = JSON.parse(
|
|
17569
|
+
const parsed = JSON.parse(readFileSync7(registryPath, "utf-8"));
|
|
16850
17570
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
16851
17571
|
return {};
|
|
16852
17572
|
const out = {};
|
|
@@ -16861,7 +17581,7 @@ function readRegistry(registryPath = defaultRegistryPath()) {
|
|
|
16861
17581
|
}
|
|
16862
17582
|
function canonicalise(p) {
|
|
16863
17583
|
try {
|
|
16864
|
-
return
|
|
17584
|
+
return realpathSync2(p);
|
|
16865
17585
|
} catch {
|
|
16866
17586
|
return p;
|
|
16867
17587
|
}
|
|
@@ -16893,9 +17613,41 @@ function runResolveEngagement(cwd, registryPath) {
|
|
|
16893
17613
|
return 0;
|
|
16894
17614
|
}
|
|
16895
17615
|
|
|
17616
|
+
// dist/refresh-credential.js
|
|
17617
|
+
var UNAUTHORIZED2 = 401;
|
|
17618
|
+
async function runRefreshCredential(opts) {
|
|
17619
|
+
if (readNeverConfiguredReason() === "not-this-account")
|
|
17620
|
+
return 0;
|
|
17621
|
+
if (!readEnvFilePath())
|
|
17622
|
+
return 0;
|
|
17623
|
+
const configResult = readConfig();
|
|
17624
|
+
if (!configResult.config)
|
|
17625
|
+
return 0;
|
|
17626
|
+
const { guardServiceToken, guardEngagementId } = configResult.config;
|
|
17627
|
+
const controlOrigin = readControlOrigin().origin;
|
|
17628
|
+
const fetchFn = opts?.fetchFn ?? globalThis.fetch;
|
|
17629
|
+
const accountCredential = readAccountCredential(controlOrigin, opts?.home);
|
|
17630
|
+
if (accountCredential)
|
|
17631
|
+
await probeAccount(controlOrigin, accountCredential, fetchFn);
|
|
17632
|
+
const probe = await probeAccount(controlOrigin, guardServiceToken, fetchFn);
|
|
17633
|
+
if (!probe.reached || probe.status !== UNAUTHORIZED2)
|
|
17634
|
+
return 0;
|
|
17635
|
+
const refreshed = await refreshEngagementCredential({
|
|
17636
|
+
engagementId: guardEngagementId,
|
|
17637
|
+
refusedToken: guardServiceToken,
|
|
17638
|
+
fetchFn,
|
|
17639
|
+
home: opts?.home
|
|
17640
|
+
});
|
|
17641
|
+
if (!refreshed.ok) {
|
|
17642
|
+
const write = opts?.write ?? ((text) => process.stdout.write(text));
|
|
17643
|
+
write(credentialRenewalFailedMessage(refreshed.remedy));
|
|
17644
|
+
}
|
|
17645
|
+
return 0;
|
|
17646
|
+
}
|
|
17647
|
+
|
|
16896
17648
|
// dist/capture.js
|
|
16897
|
-
import { readFileSync as
|
|
16898
|
-
import { dirname as
|
|
17649
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "node:fs";
|
|
17650
|
+
import { dirname as dirname4 } from "node:path";
|
|
16899
17651
|
function runCapture(provider, endpoint, opts = {}) {
|
|
16900
17652
|
const repoRoot = opts.repoRoot ?? process.cwd();
|
|
16901
17653
|
const raw = opts.input ?? readInputFile(opts.fromFile);
|
|
@@ -16930,8 +17682,8 @@ function runCapture(provider, endpoint, opts = {}) {
|
|
|
16930
17682
|
const fixture = fixturePath(repoRoot, provider, slug);
|
|
16931
17683
|
const manifestFile = manifestPath(repoRoot);
|
|
16932
17684
|
try {
|
|
16933
|
-
mkdirSync3(
|
|
16934
|
-
|
|
17685
|
+
mkdirSync3(dirname4(fixture), { recursive: true });
|
|
17686
|
+
writeFileSync4(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
16935
17687
|
} catch (err) {
|
|
16936
17688
|
const msg = err instanceof Error ? err.message : String(err);
|
|
16937
17689
|
return { ok: false, kind: "write-failed", message: `could not write fixture: ${msg}` };
|
|
@@ -16962,7 +17714,7 @@ function readInputFile(fromFile) {
|
|
|
16962
17714
|
if (!fromFile)
|
|
16963
17715
|
return void 0;
|
|
16964
17716
|
try {
|
|
16965
|
-
return
|
|
17717
|
+
return readFileSync8(fromFile, "utf-8");
|
|
16966
17718
|
} catch {
|
|
16967
17719
|
return void 0;
|
|
16968
17720
|
}
|
|
@@ -16981,6 +17733,11 @@ async function main() {
|
|
|
16981
17733
|
process.exit(code);
|
|
16982
17734
|
return;
|
|
16983
17735
|
}
|
|
17736
|
+
if (cmd === "refresh-credential") {
|
|
17737
|
+
const code = await runRefreshCredential();
|
|
17738
|
+
process.exit(code);
|
|
17739
|
+
return;
|
|
17740
|
+
}
|
|
16984
17741
|
if (cmd === "resolve-engagement") {
|
|
16985
17742
|
const cwd = sub ?? process.cwd();
|
|
16986
17743
|
const code = runResolveEngagement(cwd);
|
|
@@ -17045,6 +17802,7 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
|
|
|
17045
17802
|
halfcycle-runner hook post-tool-use
|
|
17046
17803
|
halfcycle-runner hook stop
|
|
17047
17804
|
halfcycle-runner hook subagent-stop
|
|
17805
|
+
halfcycle-runner refresh-credential
|
|
17048
17806
|
halfcycle-runner resolve-engagement <cwd>
|
|
17049
17807
|
halfcycle-runner capture <provider> <endpoint> [--from <file>]
|
|
17050
17808
|
`);
|