agent-relay 9.2.2 → 9.2.4
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/dist/cli/commands/integration-cleanup-journal.d.ts +120 -0
- package/dist/cli/commands/integration-cleanup-journal.d.ts.map +1 -0
- package/dist/cli/commands/integration-cleanup-journal.js +299 -0
- package/dist/cli/commands/integration-cleanup-journal.js.map +1 -0
- package/dist/cli/commands/integration.d.ts +72 -0
- package/dist/cli/commands/integration.d.ts.map +1 -1
- package/dist/cli/commands/integration.js +983 -55
- package/dist/cli/commands/integration.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/package.json +8 -8
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable, project-local journal of external cleanup work that failed or may
|
|
3
|
+
* have been orphaned by a crash. It exists because several ids have no other
|
|
4
|
+
* durable home once their run ends:
|
|
5
|
+
*
|
|
6
|
+
* - a superseded relayfile-cloud webhook subscription id is overwritten in the
|
|
7
|
+
* binding record the moment the replacement bind() lands;
|
|
8
|
+
* - a rollback after a failed subscribe tears down resources that were never
|
|
9
|
+
* bound anywhere;
|
|
10
|
+
* - a crash between any remote create and persisting its server-assigned id
|
|
11
|
+
* leaves a live resource findable only by a deterministic recovery key —
|
|
12
|
+
* recorded here as a `subscribe-attempt` entry written BEFORE the first
|
|
13
|
+
* create and upgraded with concrete ids as they are assigned.
|
|
14
|
+
*
|
|
15
|
+
* Entries are retried best-effort on later subscribe/unsubscribe runs and are
|
|
16
|
+
* scoped to the connection identity that created them, so a sweep never
|
|
17
|
+
* deletes resources through the wrong workspace/daemon. An entry carrying an
|
|
18
|
+
* `owner` is the LEASE of a live transaction: other processes must neither
|
|
19
|
+
* sweep it nor clear it unless the owner is provably dead on this host.
|
|
20
|
+
*
|
|
21
|
+
* The journal file lives in the CLI's project data dir (`.agentworkforce/relay/`,
|
|
22
|
+
* excluded from git by @agent-relay/config's ensureGitExclude) and is written
|
|
23
|
+
* 0600 via temp-file write + fsync + atomic rename + parent-directory fsync,
|
|
24
|
+
* so a committed update survives host/power crashes, and every
|
|
25
|
+
* read-modify-write runs under an exclusive lockfile so concurrent CLI
|
|
26
|
+
* processes cannot lose entries. A corrupt or unreadable journal FAILS
|
|
27
|
+
* CLOSED: reads throw, nothing is swept, and callers abort before creating
|
|
28
|
+
* new external resources.
|
|
29
|
+
*
|
|
30
|
+
* Attempt entries carry the inbound-target `url`, which may be capability
|
|
31
|
+
* bearing — callers must never log entry urls or ids (the 0600 file is their
|
|
32
|
+
* only home).
|
|
33
|
+
*/
|
|
34
|
+
export type PendingCleanupKind = 'relayfile-webhook-subscription' | 'relay-webhook' | 'relay-subscription' | 'subscribe-attempt';
|
|
35
|
+
export interface PendingCleanupOwner {
|
|
36
|
+
pid: number;
|
|
37
|
+
host: string;
|
|
38
|
+
/** Unique per transaction, so a run only ever clears ITS OWN entries. */
|
|
39
|
+
attemptId: string;
|
|
40
|
+
/**
|
|
41
|
+
* Epoch-ms lease heartbeat, stamped when the owner takes (or re-takes) the
|
|
42
|
+
* lease. Liveness requires BOTH a passing pid probe AND a fresh heartbeat:
|
|
43
|
+
* PID reuse (or an unprobeable foreign host) can therefore wedge later
|
|
44
|
+
* lifecycles for at most the bounded lease window, never permanently.
|
|
45
|
+
*/
|
|
46
|
+
heartbeatAt: number;
|
|
47
|
+
}
|
|
48
|
+
export interface PendingCleanupEntry {
|
|
49
|
+
kind: PendingCleanupKind;
|
|
50
|
+
/** Connection identity that may retry this entry (never a secret). */
|
|
51
|
+
scope: string;
|
|
52
|
+
/**
|
|
53
|
+
* Lifecycle operation a `subscribe-attempt` reservation belongs to. Both
|
|
54
|
+
* operations share one per-(scope, provider, resource) lease so a
|
|
55
|
+
* subscribe and an unsubscribe can never interleave on the same binding;
|
|
56
|
+
* an `unsubscribe` reservation creates nothing, so it carries no recovery
|
|
57
|
+
* keys and a dead one is simply dropped.
|
|
58
|
+
*/
|
|
59
|
+
operation?: 'subscribe' | 'unsubscribe';
|
|
60
|
+
/** Concrete resource id — required for the three concrete kinds. */
|
|
61
|
+
id?: string;
|
|
62
|
+
/**
|
|
63
|
+
* relayfile-cloud workspace the resource lives in, when known. Sweeps pin
|
|
64
|
+
* delete/list calls to it, and a cloud 404 only counts as convergence when
|
|
65
|
+
* the entry is pinned — an unpinned 404 might just be the wrong active
|
|
66
|
+
* workspace, so the entry is retained instead.
|
|
67
|
+
*/
|
|
68
|
+
relayfileWorkspaceId?: string;
|
|
69
|
+
/**
|
|
70
|
+
* subscribe-attempt recovery keys. `url`+`pathGlobs` deterministically find
|
|
71
|
+
* the relayfile-cloud subscription; the exact `webhookName` finds the relay
|
|
72
|
+
* inbound webhook; `writebackUrl` (+ binding references) finds the relay
|
|
73
|
+
* event subscription. `relayScope` scopes the relay-side recovery.
|
|
74
|
+
*/
|
|
75
|
+
provider?: string;
|
|
76
|
+
resource?: string;
|
|
77
|
+
url?: string;
|
|
78
|
+
pathGlobs?: string[];
|
|
79
|
+
/** Exact inbound webhook name (chosen before create, unique per attempt) —
|
|
80
|
+
* recovery matches on it precisely so it can never delete a DIFFERENT
|
|
81
|
+
* attempt's pre-bind webhook for the same resource. */
|
|
82
|
+
webhookName?: string;
|
|
83
|
+
writebackUrl?: string;
|
|
84
|
+
relayScope?: string;
|
|
85
|
+
/** Concrete ids, filled in as each create returns (best-effort). */
|
|
86
|
+
webhookId?: string;
|
|
87
|
+
webhookSubscriptionId?: string;
|
|
88
|
+
subscriptionId?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Present while a live transaction owns this entry (its lease across
|
|
91
|
+
* remote creates → bind). Terminal failure records are written without an
|
|
92
|
+
* owner so any later run may retry them.
|
|
93
|
+
*/
|
|
94
|
+
owner?: PendingCleanupOwner;
|
|
95
|
+
}
|
|
96
|
+
export interface CleanupJournal {
|
|
97
|
+
/** Snapshot of the journal. Throws CleanupJournalError if unreadable/corrupt. */
|
|
98
|
+
list(): Promise<PendingCleanupEntry[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Atomic read-modify-write under the exclusive journal lock. The mutation
|
|
101
|
+
* may be async — remote lifecycle actions that must serialize with
|
|
102
|
+
* reservations (e.g. URL-keyed recovery deletes) run inside it. Throws
|
|
103
|
+
* CleanupJournalError when the journal is corrupt or the lock cannot be
|
|
104
|
+
* acquired — callers must treat that as fail-closed.
|
|
105
|
+
*/
|
|
106
|
+
update(mutate: (entries: PendingCleanupEntry[]) => PendingCleanupEntry[] | Promise<PendingCleanupEntry[]>): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
export declare class CleanupJournalError extends Error {
|
|
109
|
+
readonly code: 'corrupt' | 'locked' | 'io';
|
|
110
|
+
constructor(code: 'corrupt' | 'locked' | 'io', message: string);
|
|
111
|
+
}
|
|
112
|
+
/** Stable identity for dedup/removal. Order-insensitive over pathGlobs; an
|
|
113
|
+
* owned entry is keyed by its attemptId too, so a transaction only ever adds,
|
|
114
|
+
* upgrades, or clears ITS OWN entries and never a concurrent attempt's. The
|
|
115
|
+
* delimiter is an escaped NUL, which cannot occur in any component. */
|
|
116
|
+
export declare function cleanupEntryKey(entry: PendingCleanupEntry): string;
|
|
117
|
+
/** File-backed CleanupJournal in the project data dir. Paths resolve per call.
|
|
118
|
+
* `dataDirOverride` exists for tests only — production callers use the default. */
|
|
119
|
+
export declare function fileCleanupJournal(dataDirOverride?: string): CleanupJournal;
|
|
120
|
+
//# sourceMappingURL=integration-cleanup-journal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration-cleanup-journal.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/integration-cleanup-journal.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,MAAM,MAAM,kBAAkB,GAC1B,gCAAgC,GAChC,eAAe,GACf,oBAAoB,GACpB,mBAAmB,CAAC;AAExB,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,kBAAkB,CAAC;IACzB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC;IACxC,oEAAoE;IACpE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB;;2DAEuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,IAAI,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;IACvC;;;;;;OAMG;IACH,MAAM,CACJ,MAAM,EAAE,CAAC,OAAO,EAAE,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,GACjG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,IAAI,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI;gBAAjC,IAAI,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI,EACjD,OAAO,EAAE,MAAM;CAKlB;AAED;;;uEAGuE;AACvE,wBAAgB,eAAe,CAAC,KAAK,EAAE,mBAAmB,GAAG,MAAM,CAYlE;AAkTD;mFACmF;AACnF,wBAAgB,kBAAkB,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,cAAc,CA0C3E"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { getProjectPaths } from '@agent-relay/config';
|
|
6
|
+
import { formatBrokerNotFoundError, getBrokerBinaryPath } from '@agent-relay/harness-driver/broker-path';
|
|
7
|
+
export class CleanupJournalError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = 'CleanupJournalError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Stable identity for dedup/removal. Order-insensitive over pathGlobs; an
|
|
16
|
+
* owned entry is keyed by its attemptId too, so a transaction only ever adds,
|
|
17
|
+
* upgrades, or clears ITS OWN entries and never a concurrent attempt's. The
|
|
18
|
+
* delimiter is an escaped NUL, which cannot occur in any component. */
|
|
19
|
+
export function cleanupEntryKey(entry) {
|
|
20
|
+
const globs = [...(entry.pathGlobs ?? [])].sort().join(',');
|
|
21
|
+
return [
|
|
22
|
+
entry.kind,
|
|
23
|
+
entry.scope,
|
|
24
|
+
entry.id ?? '',
|
|
25
|
+
entry.provider ?? '',
|
|
26
|
+
entry.resource ?? '',
|
|
27
|
+
entry.url ?? '',
|
|
28
|
+
globs,
|
|
29
|
+
entry.owner?.attemptId ?? '',
|
|
30
|
+
].join('\u0000');
|
|
31
|
+
}
|
|
32
|
+
const LOCK_TIMEOUT_MS = 5_000;
|
|
33
|
+
const CONCRETE_KINDS = new Set([
|
|
34
|
+
'relayfile-webhook-subscription',
|
|
35
|
+
'relay-webhook',
|
|
36
|
+
'relay-subscription',
|
|
37
|
+
]);
|
|
38
|
+
function isNonEmptyString(value) {
|
|
39
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
40
|
+
}
|
|
41
|
+
function isOptionalString(value) {
|
|
42
|
+
return value === undefined || isNonEmptyString(value);
|
|
43
|
+
}
|
|
44
|
+
function isValidOwner(owner) {
|
|
45
|
+
if (owner === undefined)
|
|
46
|
+
return true;
|
|
47
|
+
if (!owner || typeof owner !== 'object')
|
|
48
|
+
return false;
|
|
49
|
+
const candidate = owner;
|
|
50
|
+
return (typeof candidate.pid === 'number' &&
|
|
51
|
+
Number.isInteger(candidate.pid) &&
|
|
52
|
+
candidate.pid > 0 &&
|
|
53
|
+
isNonEmptyString(candidate.host) &&
|
|
54
|
+
isNonEmptyString(candidate.attemptId) &&
|
|
55
|
+
typeof candidate.heartbeatAt === 'number' &&
|
|
56
|
+
Number.isFinite(candidate.heartbeatAt) &&
|
|
57
|
+
candidate.heartbeatAt > 0);
|
|
58
|
+
}
|
|
59
|
+
/** Discriminated validation — anything short of a well-formed entry is corrupt,
|
|
60
|
+
* so a mangled journal can never be silently treated as "nothing pending". */
|
|
61
|
+
function isValidEntry(value) {
|
|
62
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
63
|
+
return false;
|
|
64
|
+
const entry = value;
|
|
65
|
+
if (!isNonEmptyString(entry.scope))
|
|
66
|
+
return false;
|
|
67
|
+
if (!isValidOwner(entry.owner))
|
|
68
|
+
return false;
|
|
69
|
+
if (!isOptionalString(entry.relayfileWorkspaceId))
|
|
70
|
+
return false;
|
|
71
|
+
if (CONCRETE_KINDS.has(entry.kind))
|
|
72
|
+
return isNonEmptyString(entry.id);
|
|
73
|
+
if (entry.kind === 'subscribe-attempt') {
|
|
74
|
+
if (entry.operation !== undefined &&
|
|
75
|
+
entry.operation !== 'subscribe' &&
|
|
76
|
+
entry.operation !== 'unsubscribe') {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (entry.operation === 'unsubscribe') {
|
|
80
|
+
return isNonEmptyString(entry.provider) && isNonEmptyString(entry.resource);
|
|
81
|
+
}
|
|
82
|
+
return (isNonEmptyString(entry.url) &&
|
|
83
|
+
isNonEmptyString(entry.provider) &&
|
|
84
|
+
isNonEmptyString(entry.resource) &&
|
|
85
|
+
isNonEmptyString(entry.webhookName) &&
|
|
86
|
+
isNonEmptyString(entry.writebackUrl) &&
|
|
87
|
+
isNonEmptyString(entry.relayScope) &&
|
|
88
|
+
Array.isArray(entry.pathGlobs) &&
|
|
89
|
+
entry.pathGlobs.length > 0 &&
|
|
90
|
+
entry.pathGlobs.every(isNonEmptyString) &&
|
|
91
|
+
isOptionalString(entry.webhookId) &&
|
|
92
|
+
isOptionalString(entry.webhookSubscriptionId) &&
|
|
93
|
+
isOptionalString(entry.subscriptionId));
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
function parseEntries(raw, file) {
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(raw);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw new CleanupJournalError('corrupt', `The pending-cleanup journal at ${file} is not valid JSON. Nothing was swept or overwritten; inspect it (it may reference external resources that still need deletion) and remove it to continue.`);
|
|
104
|
+
}
|
|
105
|
+
if (!Array.isArray(parsed) || !parsed.every(isValidEntry)) {
|
|
106
|
+
throw new CleanupJournalError('corrupt', `The pending-cleanup journal at ${file} has an unexpected shape. Nothing was swept or overwritten; inspect it and remove it to continue.`);
|
|
107
|
+
}
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
async function beginJournalTransaction(lockFile, journalFile) {
|
|
111
|
+
const binary = getBrokerBinaryPath();
|
|
112
|
+
if (!binary) {
|
|
113
|
+
throw new CleanupJournalError('io', formatBrokerNotFoundError());
|
|
114
|
+
}
|
|
115
|
+
const child = spawn(binary, [
|
|
116
|
+
'journal-lock',
|
|
117
|
+
'--file',
|
|
118
|
+
lockFile,
|
|
119
|
+
'--journal-file',
|
|
120
|
+
journalFile,
|
|
121
|
+
'--timeout-ms',
|
|
122
|
+
String(LOCK_TIMEOUT_MS),
|
|
123
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
124
|
+
const exit = new Promise((resolve) => {
|
|
125
|
+
child.on('exit', (code) => resolve(code));
|
|
126
|
+
child.on('error', () => resolve(null));
|
|
127
|
+
});
|
|
128
|
+
let exited = false;
|
|
129
|
+
void exit.then(() => {
|
|
130
|
+
exited = true;
|
|
131
|
+
});
|
|
132
|
+
// Exact framed handshake: buffer stdout until the first newline and demand
|
|
133
|
+
// the line be precisely `locked` — no substring matching, and a hard
|
|
134
|
+
// startup deadline that kills a wedged helper.
|
|
135
|
+
await new Promise((resolve, reject) => {
|
|
136
|
+
let settled = false;
|
|
137
|
+
let buffer = '';
|
|
138
|
+
const settle = (fn) => {
|
|
139
|
+
if (settled)
|
|
140
|
+
return;
|
|
141
|
+
settled = true;
|
|
142
|
+
clearTimeout(deadline);
|
|
143
|
+
fn();
|
|
144
|
+
};
|
|
145
|
+
const deadline = setTimeout(() => {
|
|
146
|
+
settle(() => {
|
|
147
|
+
child.kill('SIGKILL');
|
|
148
|
+
reject(new CleanupJournalError('io', `The journal-lock helper did not complete its handshake for ${lockFile} in time and was killed.`));
|
|
149
|
+
});
|
|
150
|
+
}, LOCK_TIMEOUT_MS + 2_000);
|
|
151
|
+
child.stdout.on('data', (chunk) => {
|
|
152
|
+
buffer += chunk.toString('utf8');
|
|
153
|
+
const newline = buffer.indexOf('\n');
|
|
154
|
+
if (newline < 0)
|
|
155
|
+
return;
|
|
156
|
+
const line = buffer.slice(0, newline);
|
|
157
|
+
if (line === 'locked') {
|
|
158
|
+
settle(resolve);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
settle(() => {
|
|
162
|
+
child.kill('SIGKILL');
|
|
163
|
+
reject(new CleanupJournalError('io', `The journal-lock helper sent an unexpected handshake for ${lockFile}.`));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
child.on('error', (err) => settle(() => reject(new CleanupJournalError('io', `Could not spawn the journal-lock helper (${binary}): ${err.message}`))));
|
|
168
|
+
void exit.then((code) => {
|
|
169
|
+
settle(() => {
|
|
170
|
+
if (code === 4) {
|
|
171
|
+
reject(new CleanupJournalError('locked', `Timed out waiting for the pending-cleanup journal lock at ${lockFile}: another agent-relay process holds it. The kernel releases it automatically when that process exits; retry afterwards.`));
|
|
172
|
+
}
|
|
173
|
+
else if (code === 3) {
|
|
174
|
+
reject(new CleanupJournalError('locked', `The pending-cleanup journal lock at ${lockFile} contains an unrecognized sentinel from an older format. Nothing was swept or overwritten; inspect and remove the file to continue.`));
|
|
175
|
+
}
|
|
176
|
+
else if (code === 2) {
|
|
177
|
+
// clap usage error: this broker binary predates the journal-lock
|
|
178
|
+
// helper (version skew), or the invocation is malformed.
|
|
179
|
+
reject(new CleanupJournalError('io', `The resolved agent-relay-broker binary (${binary}) does not support the journal-lock helper; upgrade the broker (or set AGENT_RELAY_BIN to a current build) and retry.`));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
reject(new CleanupJournalError('io', `The journal-lock helper exited unexpectedly (code ${code ?? 'unknown'}) before acquiring ${lockFile}.`));
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
const finish = async (payload, failure) => {
|
|
188
|
+
const committing = payload !== undefined;
|
|
189
|
+
if (exited) {
|
|
190
|
+
if (committing) {
|
|
191
|
+
// The lock (and with it transactional isolation) is already gone; the
|
|
192
|
+
// journal is untouched — surface instead of committing blind.
|
|
193
|
+
throw new CleanupJournalError('io', failure);
|
|
194
|
+
}
|
|
195
|
+
return; // abort: nothing left to release
|
|
196
|
+
}
|
|
197
|
+
// The helper can die between the `exited` check and (or during) the
|
|
198
|
+
// stdin write: swallow the stream error (EPIPE would otherwise be an
|
|
199
|
+
// unhandled 'error' event) and never hang on an end-callback that will
|
|
200
|
+
// no longer fire — the authoritative verdict is the exit code below.
|
|
201
|
+
await new Promise((resolve) => {
|
|
202
|
+
let settled = false;
|
|
203
|
+
const settle = () => {
|
|
204
|
+
if (settled)
|
|
205
|
+
return;
|
|
206
|
+
settled = true;
|
|
207
|
+
resolve();
|
|
208
|
+
};
|
|
209
|
+
const stdin = child.stdin;
|
|
210
|
+
stdin.once('error', settle);
|
|
211
|
+
void exit.then(settle);
|
|
212
|
+
try {
|
|
213
|
+
if (committing) {
|
|
214
|
+
stdin.end(payload, settle);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
stdin.end(settle);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
settle();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
const code = await exit;
|
|
225
|
+
if (committing && code !== 0) {
|
|
226
|
+
throw new CleanupJournalError('io', failure);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
async abort() {
|
|
231
|
+
if (exited)
|
|
232
|
+
return;
|
|
233
|
+
await finish(undefined, '');
|
|
234
|
+
},
|
|
235
|
+
async commit(payload) {
|
|
236
|
+
// Framed envelope: magic/version + declared byte length + SHA-256 of
|
|
237
|
+
// the payload. The helper commits ONLY after both validate exactly, so
|
|
238
|
+
// a parent dying mid-write (partial bytes + clean EOF) can never
|
|
239
|
+
// replace good journal contents with a truncated payload. Frame
|
|
240
|
+
// construction failures release the lock (abort) instead of leaving
|
|
241
|
+
// the helper's stdin open.
|
|
242
|
+
let framed;
|
|
243
|
+
try {
|
|
244
|
+
const body = Buffer.from(payload, 'utf8');
|
|
245
|
+
const digest = createHash('sha256').update(body).digest('hex');
|
|
246
|
+
framed = Buffer.concat([Buffer.from(`ARJL1\n${body.byteLength}\n${digest}\n`, 'utf8'), body]);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
await finish(undefined, '');
|
|
250
|
+
throw new CleanupJournalError('io', `Could not frame the pending-cleanup journal payload: ${err instanceof Error ? err.message : String(err)}`);
|
|
251
|
+
}
|
|
252
|
+
await finish(framed, `The journal-lock helper was lost before the pending-cleanup journal at ${journalFile} could be committed; the journal was left unmodified.`);
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** File-backed CleanupJournal in the project data dir. Paths resolve per call.
|
|
257
|
+
* `dataDirOverride` exists for tests only — production callers use the default. */
|
|
258
|
+
export function fileCleanupJournal(dataDirOverride) {
|
|
259
|
+
const journalFile = () => path.join(dataDirOverride ?? getProjectPaths().dataDir, 'pending-cleanups.json');
|
|
260
|
+
const read = (file) => {
|
|
261
|
+
let raw;
|
|
262
|
+
try {
|
|
263
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
if (err.code === 'ENOENT')
|
|
267
|
+
return [];
|
|
268
|
+
throw new CleanupJournalError('io', `Could not read the pending-cleanup journal at ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
269
|
+
}
|
|
270
|
+
return parseEntries(raw, file);
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
async list() {
|
|
274
|
+
return read(journalFile());
|
|
275
|
+
},
|
|
276
|
+
async update(mutate) {
|
|
277
|
+
const file = journalFile();
|
|
278
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
279
|
+
const transaction = await beginJournalTransaction(`${file}.lock`, file);
|
|
280
|
+
// EVERYTHING that can throw before the commit — the read, the mutator,
|
|
281
|
+
// and the serialization itself — runs inside the abort-protected block,
|
|
282
|
+
// or a failure would leave the helper's stdin open and the lock held
|
|
283
|
+
// until this process exits.
|
|
284
|
+
let payload;
|
|
285
|
+
try {
|
|
286
|
+
const next = await mutate(read(file));
|
|
287
|
+
payload = `${JSON.stringify(next)}\n`;
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
await transaction.abort();
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
// The helper performs the durable replacement (temp + fsync + atomic
|
|
294
|
+
// rename + dir fsync) while it still holds the kernel lock.
|
|
295
|
+
await transaction.commit(payload);
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=integration-cleanup-journal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration-cleanup-journal.js","sourceRoot":"","sources":["../../../src/cli/commands/integration-cleanup-journal.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import { type CleanupJournal } from './integration-cleanup-journal.js';
|
|
2
3
|
import { type SdkCommandDeps } from '../lib/sdk-command.js';
|
|
4
|
+
import type { SdkClientOptions } from '../lib/sdk-client.js';
|
|
3
5
|
import { MIN_RELAYFILE_VERSION, assertRelayfileVersion, type RelayfileClientOptions } from '@relayfile/client';
|
|
4
6
|
export { MIN_RELAYFILE_VERSION, assertRelayfileVersion };
|
|
5
7
|
export interface LocalRelayOptions {
|
|
@@ -12,6 +14,11 @@ export interface RelayfileBinding {
|
|
|
12
14
|
channel: string;
|
|
13
15
|
webhookId: string;
|
|
14
16
|
subscriptionId: string;
|
|
17
|
+
/** relayfile-cloud subscription that forwards provider records to the inbound webhook. */
|
|
18
|
+
webhookSubscriptionId?: string;
|
|
19
|
+
/** Workspace that subscription was created in — pins cleanup deletes across
|
|
20
|
+
* active-workspace switches. */
|
|
21
|
+
webhookSubscriptionWorkspaceId?: string;
|
|
15
22
|
}
|
|
16
23
|
export interface RelayfileResolvedResource {
|
|
17
24
|
/** Canonical VFS path glob relayfile stores the binding under (begins with '/'). */
|
|
@@ -24,6 +31,15 @@ export interface RelayfileWritebackBinding {
|
|
|
24
31
|
url: string;
|
|
25
32
|
/** Per-channel HMAC secret the subscription signs deliveries with. */
|
|
26
33
|
secret: string;
|
|
34
|
+
/** Workspace that resolved this binding (daemons >= relayfile#346) — the
|
|
35
|
+
* pre-create workspace pin for the cleanup journal. */
|
|
36
|
+
workspaceId?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface RelayfileWebhookSubscription {
|
|
39
|
+
subscriptionId: string;
|
|
40
|
+
secret?: string;
|
|
41
|
+
/** Workspace the subscription was created in (daemons >= relayfile#346). */
|
|
42
|
+
workspaceId?: string;
|
|
27
43
|
}
|
|
28
44
|
export interface RelayfileBridge {
|
|
29
45
|
isConnected(provider: string): Promise<boolean>;
|
|
@@ -35,6 +51,8 @@ export interface RelayfileBridge {
|
|
|
35
51
|
webhookId: string;
|
|
36
52
|
webhookToken: string;
|
|
37
53
|
subscriptionId: string;
|
|
54
|
+
webhookSubscriptionId: string;
|
|
55
|
+
webhookSubscriptionWorkspaceId?: string;
|
|
38
56
|
}): Promise<void>;
|
|
39
57
|
listBindings(): Promise<RelayfileBinding[]>;
|
|
40
58
|
unbind(provider: string, resource: string): Promise<void>;
|
|
@@ -61,10 +79,33 @@ export interface RelayfileBridge {
|
|
|
61
79
|
* subscription and the ingress agree on it without any static shared secret.
|
|
62
80
|
*/
|
|
63
81
|
resolveWritebackBinding(channel: string): Promise<RelayfileWritebackBinding | undefined>;
|
|
82
|
+
createWebhookSubscription(input: {
|
|
83
|
+
url: string;
|
|
84
|
+
pathGlobs: string[];
|
|
85
|
+
secret: string;
|
|
86
|
+
/** Pins the create to the pre-resolved workspace, so the created
|
|
87
|
+
* subscription can never land in a workspace other than the one already
|
|
88
|
+
* journaled in the attempt record. */
|
|
89
|
+
workspace?: string;
|
|
90
|
+
}): Promise<RelayfileWebhookSubscription>;
|
|
91
|
+
/** `workspace` pins the delete to the workspace the subscription was
|
|
92
|
+
* created in, guarding against active-workspace switches between runs. */
|
|
93
|
+
deleteWebhookSubscription(subscriptionId: string, workspace?: string): Promise<void>;
|
|
94
|
+
/** Lists a workspace's inbound webhook subscriptions so crash-recovery can
|
|
95
|
+
* find subscriptions whose server-assigned id was never persisted. */
|
|
96
|
+
listWebhookSubscriptions: (workspace?: string) => Promise<{
|
|
97
|
+
workspaceId?: string;
|
|
98
|
+
subscriptions: Array<{
|
|
99
|
+
subscriptionId: string;
|
|
100
|
+
url: string;
|
|
101
|
+
pathGlobs: string[];
|
|
102
|
+
}>;
|
|
103
|
+
}>;
|
|
64
104
|
}
|
|
65
105
|
export type IntegrationCommandDependencies = SdkCommandDeps & {
|
|
66
106
|
resolveLocalRelayOptions: () => Promise<LocalRelayOptions | undefined>;
|
|
67
107
|
relayfile: RelayfileBridge;
|
|
108
|
+
cleanupJournal: CleanupJournal;
|
|
68
109
|
isInteractive: () => boolean;
|
|
69
110
|
prompt: (question: string) => Promise<string>;
|
|
70
111
|
};
|
|
@@ -76,5 +117,36 @@ export type IntegrationCommandDependencies = SdkCommandDeps & {
|
|
|
76
117
|
* field/method drift is a typed error, not a silent runtime surprise.
|
|
77
118
|
*/
|
|
78
119
|
export declare function defaultRelayfileBridge(options?: RelayfileClientOptions): RelayfileBridge;
|
|
120
|
+
/**
|
|
121
|
+
* Non-secret identity of the relay connection a journal entry may retry
|
|
122
|
+
* through. The workspace key is a credential, so it is run through a
|
|
123
|
+
* purpose-domain PBKDF2 (never a bare hash — CodeQL
|
|
124
|
+
* js/insufficient-password-hash) with the base URL as the salt domain; only
|
|
125
|
+
* the derived digest is ever stored.
|
|
126
|
+
*/
|
|
127
|
+
export declare function relayCleanupScope(options: SdkClientOptions): string;
|
|
128
|
+
/**
|
|
129
|
+
* Bounded lease window for entry owners, set safely ABOVE every remote
|
|
130
|
+
* request timeout any lifecycle or recovery performs (each spans seconds; a
|
|
131
|
+
* reconciliation at most a few request timeouts). Explicit bounded-lease
|
|
132
|
+
* tradeoff: a process paused (e.g. SIGSTOP) past this window loses its
|
|
133
|
+
* lease and a rival may proceed — accepted deliberately, because the
|
|
134
|
+
* alternative (pure pid probing) lets PID reuse wedge lifecycles FOREVER.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Lease timing knobs, exported as one mutable object ONLY so tests can run
|
|
138
|
+
* deterministic renewal scenarios with tiny windows; production code never
|
|
139
|
+
* mutates it. renewalMs sits well below leaseMs so an active lifecycle
|
|
140
|
+
* refreshes its lease several times per window.
|
|
141
|
+
*/
|
|
142
|
+
export declare const OWNER_LEASE_CONFIG: {
|
|
143
|
+
leaseMs: number;
|
|
144
|
+
renewalMs: number;
|
|
145
|
+
/** Tolerated forward clock skew. A heartbeat further in the FUTURE than
|
|
146
|
+
* this is malformed or from a skewed clock and counts as expired —
|
|
147
|
+
* otherwise a finite future value (e.g. 9e15) would make its owner live
|
|
148
|
+
* forever and unbound the lease again. */
|
|
149
|
+
skewMs: number;
|
|
150
|
+
};
|
|
79
151
|
export declare function registerIntegrationCommands(program: Command, overrides?: Partial<IntegrationCommandDependencies>): void;
|
|
80
152
|
//# sourceMappingURL=integration.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/integration.ts"],"names":[],"mappings":"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"}
|