viber-channel 0.8.19 → 0.8.21
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/lib/agent_tools.ts +18 -1
- package/lib/bridge_core.ts +76 -17
- package/lib/bridge_spawn.ts +1 -1
- package/lib/control_stream.ts +3 -2
- package/lib/coverage_watch.ts +244 -0
- package/lib/fingerprint.ts +1 -1
- package/lib/hidden_proc.ts +99 -0
- package/lib/parent_watchdog.ts +1 -1
- package/lib/presence_record.ts +358 -0
- package/lib/process_start.ts +132 -10
- package/lib/runner_enroll.ts +26 -4
- package/lib/runner_exec.ts +229 -7
- package/lib/runner_registry.ts +413 -0
- package/lib/runner_relaunch.ts +181 -0
- package/lib/runner_roster.ts +521 -0
- package/lib/runner_stream.ts +66 -35
- package/lib/urls.ts +3 -1
- package/package.json +1 -1
- package/viber-channel.ts +65 -25
- package/viber-codex-bridge.ts +43 -9
package/lib/runner_exec.ts
CHANGED
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
* injectable so the flow is unit-testable without shelling or a live server.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { execFile } from "
|
|
16
|
+
import { execFile } from "./hidden_proc.js";
|
|
17
17
|
import { randomBytes } from "node:crypto";
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { cfAccessHeaders } from "./cfAccess.js";
|
|
21
21
|
import { gatewayNothingToDo, gatewaySpawnOk, parseGatewayResult } from "./gateway_result.js";
|
|
22
22
|
import { messageForReason, parseSpawnResult, rosterNames, type SpawnReason } from "./spawn_reason.js";
|
|
23
|
+
import { identityBaseUrl } from "./base_urls.js";
|
|
24
|
+
import { recordSpawn, registryPath, type SpawnForRegistry, updateRegistry } from "./runner_registry.js";
|
|
25
|
+
import { relaunchInstance } from "./runner_relaunch.js";
|
|
23
26
|
|
|
24
27
|
/** A validated role from the server snapshot (allowlisted shape). */
|
|
25
28
|
export interface SnapshotRole {
|
|
@@ -39,12 +42,79 @@ export interface ClaimedCommand {
|
|
|
39
42
|
prefix: string;
|
|
40
43
|
team_name: string | null;
|
|
41
44
|
env: string | null;
|
|
45
|
+
/** From the LIST entry (the claim response does not carry it). #632: the app
|
|
46
|
+
* mode maps it to a local folder; the source runner ignores it. */
|
|
47
|
+
project_id?: number;
|
|
48
|
+
/** #627: `relaunch` brings back ONE agent from the runner's registry; absent = team. */
|
|
49
|
+
kind?: "team" | "relaunch";
|
|
50
|
+
relaunch_instance_id?: string | null;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
export interface RunnerAuthLite {
|
|
45
54
|
base_url: string;
|
|
46
55
|
runner_id: string;
|
|
47
56
|
runner_token: string;
|
|
57
|
+
/** #627: the web host the machine enrolled on. Absent in a runner.json written
|
|
58
|
+
* before #627 — `runnerIdentity` then falls back to `VIBER_BASE_URL`. */
|
|
59
|
+
identity_base_url?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* #627 — the backend identity of each launch env, i.e. the `VIBER_BASE_URL`
|
|
64
|
+
* vibe-master gives an agent launched with `--env <name>`.
|
|
65
|
+
*
|
|
66
|
+
* ⚠ A COPY of `VIBER_ENVS[*].baseUrl` in `vibe-master/lib/env.ts` — viber-channel is
|
|
67
|
+
* published on its own and cannot import vibe-master. `test/runner_identity.test.ts`
|
|
68
|
+
* pins the two together, so a changed host breaks the build instead of silently
|
|
69
|
+
* sending an agent's presence record into a directory its runner never reads.
|
|
70
|
+
*/
|
|
71
|
+
export const ENV_IDENTITY: Readonly<Record<string, string>> = {
|
|
72
|
+
dev: "https://viber-dev.dgypx.dev",
|
|
73
|
+
staging: "https://viber.dgypx.dev",
|
|
74
|
+
qa: "https://viber-qa.dgypx.dev",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** The backend this runner serves, verbatim (never normalized, #480). */
|
|
78
|
+
export function runnerIdentity(auth: Pick<RunnerAuthLite, "identity_base_url">, env: NodeJS.ProcessEnv = process.env): string {
|
|
79
|
+
if (auth.identity_base_url !== undefined) return auth.identity_base_url;
|
|
80
|
+
if (!legacyIdentityAnnounced) {
|
|
81
|
+
// A runner.json from before #627: the enrolled host was not recorded, so this is a
|
|
82
|
+
// GUESS (VIBER_BASE_URL, else the default). Wrong for an app runner enrolled on dev.
|
|
83
|
+
legacyIdentityAnnounced = true;
|
|
84
|
+
process.stderr.write(
|
|
85
|
+
`[runner] runner.json has no identity_base_url (enrolled before #627): assuming ${identityBaseUrl(env)} — re-enroll to record it
|
|
86
|
+
`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return identityBaseUrl(env);
|
|
90
|
+
}
|
|
91
|
+
let legacyIdentityAnnounced = false;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The env a team must be launched with so its agents land on THIS runner's backend,
|
|
95
|
+
* or a refusal. An explicit env pointing elsewhere is refused: the agents would write
|
|
96
|
+
* their presence records under another backend, the runner would never see them, and
|
|
97
|
+
* they would stay paused forever (#627 step-06). No env → the runner's own, when the
|
|
98
|
+
* identity is a known one; else unchanged (a custom backend, as before #627).
|
|
99
|
+
*/
|
|
100
|
+
export function launchEnvFor(
|
|
101
|
+
cmdEnv: string | null,
|
|
102
|
+
identity: string,
|
|
103
|
+
): { ok: true; env: string | null } | { ok: false; error: string } {
|
|
104
|
+
if (cmdEnv !== null) {
|
|
105
|
+
const target = ENV_IDENTITY[cmdEnv];
|
|
106
|
+
if (target === identity) return { ok: true, env: cmdEnv };
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error:
|
|
110
|
+
`env "${cmdEnv}" runs agents on ${target ?? "an unknown backend"}, but this runner serves ${identity}` +
|
|
111
|
+
(target !== undefined && target.replace(/\/+$/, "") === identity.replace(/\/+$/, "")
|
|
112
|
+
? " — the enrolled host differs only by a trailing slash: re-enroll this machine"
|
|
113
|
+
: ""),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const own = Object.keys(ENV_IDENTITY).find((name) => ENV_IDENTITY[name] === identity);
|
|
117
|
+
return { ok: true, env: own ?? null };
|
|
48
118
|
}
|
|
49
119
|
|
|
50
120
|
/**
|
|
@@ -374,6 +444,8 @@ export function buildGatewayTeamArgs(cmd: {
|
|
|
374
444
|
teamName?: string;
|
|
375
445
|
specPath: string;
|
|
376
446
|
cwd: string;
|
|
447
|
+
/** #627: the project, for the runner registry the gateway writes. */
|
|
448
|
+
projectId?: number;
|
|
377
449
|
registryDir: string;
|
|
378
450
|
permission: string;
|
|
379
451
|
env?: string;
|
|
@@ -423,6 +495,8 @@ export function buildGatewayTeamArgs(cmd: {
|
|
|
423
495
|
cmd.nonce,
|
|
424
496
|
];
|
|
425
497
|
if (cmd.env) args.push("--env", cmd.env);
|
|
498
|
+
// #627: the gateway writes the team into the runner registry, keyed by project.
|
|
499
|
+
if (cmd.projectId !== undefined) args.push("--project", String(cmd.projectId));
|
|
426
500
|
// ▶ Only when set: an empty `--vibe-master-cmd` would make the gateway resolve an
|
|
427
501
|
// empty command, which is worse than not passing the flag at all.
|
|
428
502
|
if (cmd.vibeMasterCmd && cmd.vibeMasterCmd.trim().length > 0) {
|
|
@@ -452,8 +526,12 @@ export function buildGatewayTeamArgs(cmd: {
|
|
|
452
526
|
* "we could not read the verdict" must not collapse into "it worked"
|
|
453
527
|
* (`gatewaySpawnOk`).
|
|
454
528
|
*/
|
|
455
|
-
export const execFileGatewayTeamSpawn: TeamSpawnRunner = (args, nonce) =>
|
|
456
|
-
|
|
529
|
+
export const execFileGatewayTeamSpawn: TeamSpawnRunner = (args, nonce) =>
|
|
530
|
+
gatewayTeamSpawnWith(viberGatewayBaseCmd())(args, nonce);
|
|
531
|
+
|
|
532
|
+
/** #632: the same launcher with an explicit base command — the app mode passes its
|
|
533
|
+
* own absolute `process.execPath`, never a bare name resolved through PATH. */
|
|
534
|
+
export const gatewayTeamSpawnWith = (base: string[]): TeamSpawnRunner => (args, nonce) => {
|
|
457
535
|
const bin = base[0] as string;
|
|
458
536
|
return new Promise((resolve) => {
|
|
459
537
|
execFile(
|
|
@@ -540,6 +618,38 @@ export interface ExecDeps {
|
|
|
540
618
|
writeLog?: (commandId: string, recap: string) => string;
|
|
541
619
|
/** Injectable daemon-stderr writer (tests). Defaults to process.stderr. */
|
|
542
620
|
writeDaemonLine?: (line: string) => void;
|
|
621
|
+
/**
|
|
622
|
+
* #632: the folder the AGENTS work in, per project. Absent = the runner's own
|
|
623
|
+
* cwd (source runner, unchanged). `null` = no folder for this project (the user
|
|
624
|
+
* cancelled the picker): the command is REFUSED, never launched in a default.
|
|
625
|
+
*/
|
|
626
|
+
resolveWorkspace?: (projectId: number | undefined) => Promise<string | null>;
|
|
627
|
+
/** #632: `vibe-master` for the launched team, when the env var is not the source. */
|
|
628
|
+
vibeMasterCmd?: string;
|
|
629
|
+
/** #627: remember a launched team on this machine (default: the registry on disk). */
|
|
630
|
+
recordLaunch?: (spawn: SpawnForRegistry) => void;
|
|
631
|
+
/** #627: runs `vibe-master <args>` for a relaunch (default: execFile, like a team). */
|
|
632
|
+
runSingleSpawn?: (args: string[]) => Promise<{ ok: boolean; recap: string }>;
|
|
633
|
+
/** #627: relaunch seams (tests). */
|
|
634
|
+
relaunchLockDir?: string;
|
|
635
|
+
relaunchEnumerate?: () => Promise<Map<number, number | undefined> | undefined>;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* #627 — the default `recordLaunch`: write the team into this backend's registry.
|
|
640
|
+
* The backend is `runnerIdentity(auth)` — the enrolled web host, which `launchEnvFor`
|
|
641
|
+
* guarantees is also the agents' `VIBER_BASE_URL`, so the registry sits in the
|
|
642
|
+
* directory their presence records are written to. `auth.base_url` would be wrong
|
|
643
|
+
* here: it may be the announced API host (#480), not the identity.
|
|
644
|
+
*/
|
|
645
|
+
export function recordLaunchOnDisk(
|
|
646
|
+
identity: string,
|
|
647
|
+
spawn: SpawnForRegistry,
|
|
648
|
+
log: (line: string) => void = (l) => process.stderr.write(`${l}\n`),
|
|
649
|
+
now: () => number = Date.now,
|
|
650
|
+
): void {
|
|
651
|
+
const result = updateRegistry(registryPath(identity), (r) => recordSpawn(r, spawn, now()));
|
|
652
|
+
if (!result.ok) log(`[runner] registry NOT updated for command=${spawn.command_id}: ${result.error}`);
|
|
543
653
|
}
|
|
544
654
|
|
|
545
655
|
/** Where the log lives, as shown to whoever reads the WEB (#506): RELATIVE, so
|
|
@@ -648,6 +758,7 @@ export async function executeClaim(
|
|
|
648
758
|
// talks to `vibe-master` directly, and its witnesses keep measuring that contract.
|
|
649
759
|
const runTeamSpawn = deps.runTeamSpawn ?? execFileGatewayTeamSpawn;
|
|
650
760
|
const fetchImpl = deps.apiFetch ?? fetch;
|
|
761
|
+
if (cmd.kind === "relaunch") return executeRelaunchClaim(auth, cmd, deps, fetchImpl);
|
|
651
762
|
const inflight = deps.inflight ?? fsInflightStore();
|
|
652
763
|
|
|
653
764
|
// P1-4: a PRESENT-but-invalid local policy REFUSES execution (never the
|
|
@@ -666,6 +777,19 @@ export async function executeClaim(
|
|
|
666
777
|
return "failed";
|
|
667
778
|
}
|
|
668
779
|
|
|
780
|
+
// #632: the agents' folder. Resolved BEFORE any report, so a cancelled picker
|
|
781
|
+
// refuses the command without a side effect.
|
|
782
|
+
const workspace = deps.resolveWorkspace ? await deps.resolveWorkspace(cmd.project_id) : process.cwd();
|
|
783
|
+
if (workspace === null) {
|
|
784
|
+
await report(
|
|
785
|
+
auth,
|
|
786
|
+
cmd.id,
|
|
787
|
+
refusalBody(cmd, "launch_refused", `no local folder chosen for project ${cmd.project_id ?? "?"}`),
|
|
788
|
+
fetchImpl,
|
|
789
|
+
);
|
|
790
|
+
return "failed";
|
|
791
|
+
}
|
|
792
|
+
|
|
669
793
|
const refusal = validateAgainstPolicy(cmd, policy);
|
|
670
794
|
if (refusal) {
|
|
671
795
|
await report(
|
|
@@ -677,6 +801,14 @@ export async function executeClaim(
|
|
|
677
801
|
return "failed";
|
|
678
802
|
}
|
|
679
803
|
|
|
804
|
+
// #627: the agents must land on this runner's backend, or it can never vouch for them.
|
|
805
|
+
const identity = runnerIdentity(auth);
|
|
806
|
+
const launchEnv = launchEnvFor(cmd.env, identity);
|
|
807
|
+
if (!launchEnv.ok) {
|
|
808
|
+
await report(auth, cmd.id, refusalBody(cmd, "launch_refused", launchEnv.error), fetchImpl);
|
|
809
|
+
return "failed";
|
|
810
|
+
}
|
|
811
|
+
|
|
680
812
|
// Local idempotence (C4): a marker for this command_id means a PRIOR run
|
|
681
813
|
// already started it here (crashed mid-spawn, now re-claimed). Do NOT relaunch.
|
|
682
814
|
// Keep the marker unless the failed report is ACCEPTED (2xx) — else a later
|
|
@@ -746,7 +878,8 @@ export async function executeClaim(
|
|
|
746
878
|
prefix: cmd.prefix,
|
|
747
879
|
...(cmd.team_name ? { teamName: cmd.team_name } : {}),
|
|
748
880
|
specPath,
|
|
749
|
-
cwd:
|
|
881
|
+
cwd: workspace,
|
|
882
|
+
...(cmd.project_id !== undefined ? { projectId: cmd.project_id } : {}),
|
|
750
883
|
// Beside `runner-specs`/`runner-logs`/`runner-inflight`, never the bare cwd - see
|
|
751
884
|
// `buildGatewayTeamArgs`, where the measurement that forced this is recorded.
|
|
752
885
|
registryDir: ensureGatewayRegistryDir(),
|
|
@@ -757,13 +890,17 @@ export async function executeClaim(
|
|
|
757
890
|
// permission from the ARGV and never from the spec, so a mixed roster used to
|
|
758
891
|
// launch a `read-only` member `read-write` with nothing going red.
|
|
759
892
|
permission: cmd.template_spec.roles[0]?.permission ?? "read-only",
|
|
760
|
-
...(
|
|
893
|
+
...(launchEnv.env ? { env: launchEnv.env } : {}),
|
|
761
894
|
nonce,
|
|
762
895
|
// ⚠ See (3) on `buildGatewayTeamArgs`: the runner no longer launches
|
|
763
896
|
// `vibe-master`, so this must TRAVEL or dev silently uses the installed
|
|
764
897
|
// binary. Read from the env here, where the variable is already the
|
|
765
898
|
// convention, rather than re-invented as a second knob.
|
|
766
|
-
...(process.env.VIBE_MASTER_CMD
|
|
899
|
+
...(process.env.VIBE_MASTER_CMD
|
|
900
|
+
? { vibeMasterCmd: process.env.VIBE_MASTER_CMD }
|
|
901
|
+
: deps.vibeMasterCmd
|
|
902
|
+
? { vibeMasterCmd: deps.vibeMasterCmd }
|
|
903
|
+
: {}),
|
|
767
904
|
});
|
|
768
905
|
|
|
769
906
|
inflight.mark(cmd.id); // durable "started here" BEFORE launch (C4)
|
|
@@ -797,6 +934,22 @@ export async function executeClaim(
|
|
|
797
934
|
// reassuring word promoted on no proof — and it would do so while LOOKING like belt-and-braces.
|
|
798
935
|
const nothingToDo = launcherSaysNothingToDo ?? false;
|
|
799
936
|
|
|
937
|
+
// #627: a launched team is remembered on this machine, so the web can offer to
|
|
938
|
+
// relaunch its members with these settings after a crash or a reboot. Only on a
|
|
939
|
+
// real launch — "already running" launched nothing and must not reset entries.
|
|
940
|
+
if (ok) {
|
|
941
|
+
(deps.recordLaunch ?? ((s: SpawnForRegistry) => recordLaunchOnDisk(identity, s)))({
|
|
942
|
+
command_id: cmd.id,
|
|
943
|
+
template_name: cmd.template_name,
|
|
944
|
+
prefix: cmd.prefix,
|
|
945
|
+
team_name: cmd.team_name,
|
|
946
|
+
env: launchEnv.env,
|
|
947
|
+
project_id: cmd.project_id ?? null,
|
|
948
|
+
workspace,
|
|
949
|
+
roles: cmd.template_spec.roles,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
800
953
|
// #496: the failure now travels as a REASON plus a sentence built from it —
|
|
801
954
|
// whoever clicked Spawn in a browser can act on what they read. The full
|
|
802
955
|
// recap still goes ONLY to the 0600 local log (no secret leak); what changed
|
|
@@ -878,7 +1031,7 @@ export async function reconcileOnce(auth: RunnerAuthLite, deps: ExecDeps = {}):
|
|
|
878
1031
|
headers: { Authorization: `Bearer ${auth.runner_token}`, ...cfAccessHeaders() },
|
|
879
1032
|
});
|
|
880
1033
|
if (!listResp.ok) return 0;
|
|
881
|
-
const { commands } = (await listResp.json()) as { commands: { id: string }[] };
|
|
1034
|
+
const { commands } = (await listResp.json()) as { commands: { id: string; project_id?: number }[] };
|
|
882
1035
|
if (!commands.length) return 0;
|
|
883
1036
|
|
|
884
1037
|
const first = commands[0];
|
|
@@ -888,6 +1041,75 @@ export async function reconcileOnce(auth: RunnerAuthLite, deps: ExecDeps = {}):
|
|
|
888
1041
|
);
|
|
889
1042
|
if (claimResp.status !== 200) return 0; // lost the claim — another pass will retry
|
|
890
1043
|
const claimed = (await claimResp.json()) as ClaimedCommand;
|
|
1044
|
+
if (claimed.project_id === undefined && first.project_id !== undefined) claimed.project_id = first.project_id;
|
|
891
1045
|
await executeClaim(auth, claimed, deps);
|
|
892
1046
|
return 1;
|
|
893
1047
|
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* #627 step-07 — a RELAUNCH command: one agent, from this runner's registry. The
|
|
1051
|
+
* decision (refuse / re-attach / launch) is `relaunchInstance`'s; this reports it with
|
|
1052
|
+
* the same fencing-guarded reports as a team spawn. No automatic retry on failure.
|
|
1053
|
+
*/
|
|
1054
|
+
async function executeRelaunchClaim(
|
|
1055
|
+
auth: RunnerAuthLite,
|
|
1056
|
+
cmd: ClaimedCommand,
|
|
1057
|
+
deps: ExecDeps,
|
|
1058
|
+
fetchImpl: typeof fetch,
|
|
1059
|
+
): Promise<"succeeded" | "failed" | "aborted"> {
|
|
1060
|
+
const instanceId = cmd.relaunch_instance_id ?? "";
|
|
1061
|
+
if (!/^[0-9a-f]{32}$/.test(instanceId)) {
|
|
1062
|
+
await report(auth, cmd.id, refusalBody(cmd, "launch_refused", "relaunch command without a valid instance id"), fetchImpl);
|
|
1063
|
+
return "failed";
|
|
1064
|
+
}
|
|
1065
|
+
// Same local idempotence as a team (C4): a marker means a PRIOR run already started
|
|
1066
|
+
// this relaunch here — never launch a second agent for one command.
|
|
1067
|
+
const inflight = deps.inflight ?? fsInflightStore();
|
|
1068
|
+
if (inflight.has(cmd.id)) {
|
|
1069
|
+
const r = await report(auth, cmd.id, refusalBody(cmd, "launch_refused", "this relaunch already ran on this machine"), fetchImpl);
|
|
1070
|
+
if (r >= 200 && r < 300) inflight.clear(cmd.id);
|
|
1071
|
+
return "failed";
|
|
1072
|
+
}
|
|
1073
|
+
// Only a 2xx means the claim is still ours — anything else, never launch (Codex).
|
|
1074
|
+
const starting = await report(auth, cmd.id, { fencing_token: cmd.fencing_token, status: "starting" }, fetchImpl);
|
|
1075
|
+
if (starting < 200 || starting >= 300) return "aborted";
|
|
1076
|
+
inflight.mark(cmd.id);
|
|
1077
|
+
// The saved settings meet TODAY's local policy, like a team does (Codex): an operator
|
|
1078
|
+
// who restricted the machine since the first launch is obeyed.
|
|
1079
|
+
let policy: RunnerPolicy;
|
|
1080
|
+
try {
|
|
1081
|
+
policy = deps.policy ?? loadRunnerPolicy();
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
const cause = err instanceof RunnerPolicyInvalidError ? err.message : "invalid local policy";
|
|
1084
|
+
await report(auth, cmd.id, refusalBody(cmd, "policy_invalid", `local policy invalid: ${cause}`), fetchImpl);
|
|
1085
|
+
inflight.clear(cmd.id);
|
|
1086
|
+
return "failed";
|
|
1087
|
+
}
|
|
1088
|
+
const outcome = await relaunchInstance(instanceId, {
|
|
1089
|
+
identity: runnerIdentity(auth),
|
|
1090
|
+
...(deps.relaunchLockDir !== undefined ? { lockDir: deps.relaunchLockDir } : {}),
|
|
1091
|
+
...(deps.relaunchEnumerate !== undefined ? { enumerate: deps.relaunchEnumerate } : {}),
|
|
1092
|
+
runSpawn: deps.runSingleSpawn ?? ((args) => execFileTeamSpawn(args, randomBytes(8).toString("hex"))),
|
|
1093
|
+
checkPolicy: (entry) =>
|
|
1094
|
+
validateAgainstPolicy(
|
|
1095
|
+
{ ...cmd, env: entry.env, template_spec: { roles: [{ ...entry.role }] } },
|
|
1096
|
+
policy,
|
|
1097
|
+
),
|
|
1098
|
+
});
|
|
1099
|
+
// #627 (live validation): the web says "see the runner log" — so the log must carry it.
|
|
1100
|
+
// vibe-master's own output goes to the 0600 LOCAL log only, like a team's recap (#496:
|
|
1101
|
+
// it may hold secrets) — stderr can be captured elsewhere (Opus). The console line
|
|
1102
|
+
// carries only the runner-authored sentence and where the detail is.
|
|
1103
|
+
if (outcome.kind === "refused") {
|
|
1104
|
+
const logPath = outcome.recap ? (deps.writeLog ? deps.writeLog(cmd.id, outcome.recap) : writeRunnerLog(cmd.id, outcome.recap)) : undefined;
|
|
1105
|
+
console.error(`[runner] relaunch ${instanceId.slice(0, 8)} refused: ${outcome.error}${logPath ? ` (log: ${logPath})` : ""}`);
|
|
1106
|
+
}
|
|
1107
|
+
const body =
|
|
1108
|
+
outcome.kind === "refused"
|
|
1109
|
+
? refusalBody(cmd, outcome.policy ? "policy_refused" : "launch_refused", outcome.error)
|
|
1110
|
+
: { fencing_token: cmd.fencing_token, status: "succeeded", result: { message: outcome.message } };
|
|
1111
|
+
const final = await report(auth, cmd.id, body, fetchImpl);
|
|
1112
|
+
if (final < 200 || final >= 300) return "aborted"; // marker kept for reconcile
|
|
1113
|
+
inflight.clear(cmd.id);
|
|
1114
|
+
return outcome.kind === "refused" ? "failed" : "succeeded";
|
|
1115
|
+
}
|