@halofy/agent-connect 0.11.0 → 0.12.1
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/README.md +38 -4
- package/package.json +1 -1
- package/src/claude-hook.mjs +12 -6
- package/src/enrollment.mjs +83 -0
- package/src/host-hook.mjs +4 -7
- package/src/install.mjs +2 -2
- package/src/installer-cli.mjs +14 -4
- package/src/instructions.mjs +29 -14
- package/src/runtime.mjs +1 -1
- package/src/version.mjs +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Halofy agent lifecycle installer
|
|
2
2
|
|
|
3
|
-
Status:
|
|
4
|
-
|
|
3
|
+
Status: source for `@halofy/agent-connect@0.12.1`; publication and deployment
|
|
4
|
+
require separate verification. The production console supplies the command for
|
|
5
|
+
the verified published package pinned by the deployment workflow. Host loading
|
|
6
|
+
requires separate verification.
|
|
5
7
|
|
|
6
8
|
This package contains the host-neutral client pieces for installation-bound
|
|
7
9
|
agent connections:
|
|
@@ -22,10 +24,32 @@ conversations and serves the agent-invoked MCP memory tools, but does not push
|
|
|
22
24
|
recalled memory into host sessions on session start or prompt submit. The
|
|
23
25
|
bounded recall block formats stay in place and tested for when it returns.
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
## Browser sign-in
|
|
28
|
+
|
|
29
|
+
Users assigned to a team can run the installer without a manually copied badge.
|
|
30
|
+
Use the production console’s verified command. After 0.12.1 is published and
|
|
31
|
+
verified, the command for this source version is:
|
|
26
32
|
|
|
27
33
|
```bash
|
|
28
|
-
npx
|
|
34
|
+
npx @halofy/agent-connect@0.12.1 install claude-code --server https://app.halofy.ai
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`login <client-kind> --server <origin>` is an alias for the same installation
|
|
38
|
+
flow. The installer opens the existing Halofy sign-in page, displays a public
|
|
39
|
+
verification code, and waits for the user to confirm their authorized team and
|
|
40
|
+
device. Google sign-in is available through the existing console authentication.
|
|
41
|
+
A per-installation Ed25519 key is saved privately before enrollment; the approval
|
|
42
|
+
claim is bound to that same key. Device secrets and claims are never printed or
|
|
43
|
+
put in browser URLs. Retry a failed/expired sign-in with the same command; the
|
|
44
|
+
pending key survives retries. Installation still requires the terminal's
|
|
45
|
+
explicit CONNECT disclosure before changing host configuration.
|
|
46
|
+
|
|
47
|
+
Browser sign-in requires the matching server enrollment endpoints. The example
|
|
48
|
+
above does not establish that 0.12.1 is published or enabled in production. The
|
|
49
|
+
manual claim path remains supported; after publication, its 0.12.1 form is:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx --yes @halofy/agent-connect@0.12.1 install <client-kind> \
|
|
29
53
|
--server https://app.halofy.ai \
|
|
30
54
|
--claim '<one-time-claim>'
|
|
31
55
|
```
|
|
@@ -199,6 +223,16 @@ memory recall. There is no resident instruction daemon; an offline device or a
|
|
|
199
223
|
host session without an installed startup hook does not fetch updates. Claude's
|
|
200
224
|
existing hook installation is project-scoped even though its rule file is global.
|
|
201
225
|
|
|
226
|
+
Version 0.12.1 gives instruction GET and its best-effort status receipt one shared
|
|
227
|
+
8-second deadline at startup or direct refresh, and 15 seconds during interactive
|
|
228
|
+
installation. A failed GET preserves current instructions; a receipt timeout does
|
|
229
|
+
not undo a completed local update or prove server acknowledgement. Other request
|
|
230
|
+
and host-hook limits are unchanged; this is not a deadline guarantee for the
|
|
231
|
+
whole startup sequence. Startup runs instruction sync alongside skill/reference
|
|
232
|
+
delivery, including while capture is paused, so slow reference downloads or
|
|
233
|
+
receipts do not hold up the instruction fetch. Replay and heartbeat still follow
|
|
234
|
+
both refreshes.
|
|
235
|
+
|
|
202
236
|
| Host | Local target | Boundaries |
|
|
203
237
|
|---|---|---|
|
|
204
238
|
| Codex | `CODEX_HOME` or `~/.codex`, active nonempty `AGENTS.override.md` else `AGENTS.md` | Marked section; changing the active target reports a conflict |
|
package/package.json
CHANGED
package/src/claude-hook.mjs
CHANGED
|
@@ -12,6 +12,15 @@ export async function syncSkillsAtSessionStart(runtime, connection, root, stderr
|
|
|
12
12
|
return result.skills;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Disjoint owned files and locks let instructions refresh without waiting for
|
|
16
|
+
* skill/reference downloads or their receipt. Both settle before capture work. */
|
|
17
|
+
export async function syncConfigurationAtSessionStart(runtime, connection, root, stderr, syncOptions = {}) {
|
|
18
|
+
await Promise.allSettled([
|
|
19
|
+
Promise.resolve().then(() => runtime.syncInstructions?.()),
|
|
20
|
+
syncSkillsAtSessionStart(runtime, connection, root, stderr, syncOptions),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
function id(value) {
|
|
16
25
|
return createHash("sha256").update(String(value)).digest("hex");
|
|
17
26
|
}
|
|
@@ -96,9 +105,8 @@ export async function runClaudeLifecycleHook(connection, eventName, {
|
|
|
96
105
|
const runtime = runtimeFactory(connection, { root, captureGeneration: capture.generation });
|
|
97
106
|
if (capture.paused) {
|
|
98
107
|
if (eventName === "SessionStart") {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await runtime.syncInstructions?.().catch(() => {});
|
|
108
|
+
// Capture pause does not suspend instruction or reference delivery.
|
|
109
|
+
await syncConfigurationAtSessionStart(runtime, connection, root, stderr, { home, env });
|
|
102
110
|
await runtime.heartbeat(connection.capabilities || {});
|
|
103
111
|
} else if (eventName === "UserPromptSubmit") {
|
|
104
112
|
const delivery = await syncManagedDelivery({ connection, transport: runtime.transport, root, home, env, stderr, phase: "turn" });
|
|
@@ -110,9 +118,7 @@ export async function runClaudeLifecycleHook(connection, eventName, {
|
|
|
110
118
|
|
|
111
119
|
if (eventName === "SessionStart") {
|
|
112
120
|
// Refresh/withdraw local context even when replay or heartbeat fails.
|
|
113
|
-
await
|
|
114
|
-
// Instructions have their own brownout boundary, independent of recall.
|
|
115
|
-
await runtime.syncInstructions?.().catch(() => {});
|
|
121
|
+
await syncConfigurationAtSessionStart(runtime, connection, root, stderr, { home, env });
|
|
116
122
|
await runtime.replay();
|
|
117
123
|
await runtime.heartbeat(connection.capabilities || {});
|
|
118
124
|
if (RECALL_INJECTION_ENABLED) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { generateInstallationKeyPair } from "./crypto.mjs";
|
|
5
|
+
import { normalizeLifecycleServerUrl } from "./install.mjs";
|
|
6
|
+
import { readJson, writePrivateFile, defaultRuntimeDirectory } from "./storage.mjs";
|
|
7
|
+
import { CLIENT_KINDS } from "./client-registry.mjs";
|
|
8
|
+
|
|
9
|
+
export async function prepareInstallationKey(root, clientKind) {
|
|
10
|
+
const path = join(root, `pending-${clientKind}.json`);
|
|
11
|
+
const previous = await readJson(path);
|
|
12
|
+
if (previous) {
|
|
13
|
+
if (!previous.publicJwk || !previous.privateJwk || !previous.installationIdCandidate) {
|
|
14
|
+
throw new Error("pending installation key is invalid; repair local runtime storage before retrying");
|
|
15
|
+
}
|
|
16
|
+
return previous;
|
|
17
|
+
}
|
|
18
|
+
const key = { ...generateInstallationKeyPair(), installationIdCandidate: `local_${randomUUID()}` };
|
|
19
|
+
await writePrivateFile(path, `${JSON.stringify(key)}\n`);
|
|
20
|
+
return key;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function openVerificationBrowser(uri) {
|
|
24
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
25
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", uri] : [uri];
|
|
26
|
+
const child = spawn(command, args, { shell: false, detached: true, stdio: "ignore" });
|
|
27
|
+
child.on("error", () => {}); // Printed URL remains usable when there is no desktop browser.
|
|
28
|
+
child.unref();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Device secrets stay in memory. Only the public human code reaches browser/terminal. */
|
|
32
|
+
export async function enrollWithBrowser({ serverUrl, clientKind, root = defaultRuntimeDirectory(),
|
|
33
|
+
fetchImpl = globalThis.fetch, output = process.stdout, openBrowser = openVerificationBrowser,
|
|
34
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now }) {
|
|
35
|
+
if (!CLIENT_KINDS.includes(clientKind)) throw new Error("unsupported enrollment client");
|
|
36
|
+
const server = normalizeLifecycleServerUrl(serverUrl);
|
|
37
|
+
const base = new URL(server);
|
|
38
|
+
if (!["https:", "http:"].includes(base.protocol) || base.username || base.password || base.search || base.hash || base.pathname !== "/") {
|
|
39
|
+
throw new Error("sign-in requires a server origin without credentials, path or query");
|
|
40
|
+
}
|
|
41
|
+
const key = await prepareInstallationKey(root, clientKind);
|
|
42
|
+
async function request(path, body) {
|
|
43
|
+
let response;
|
|
44
|
+
try {
|
|
45
|
+
response = await fetchImpl(`${server}/v1/agent-enrollment/${path}`, {
|
|
46
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
|
|
47
|
+
redirect: "error", signal: AbortSignal.timeout(15_000),
|
|
48
|
+
});
|
|
49
|
+
} catch { throw new Error("sign-in connection unavailable; retry sign-in"); }
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(response.status === 429 ? "sign-in rate limit reached; try again later" :
|
|
52
|
+
response.status === 410 ? "sign-in expired; retry sign-in" : "sign-in denied or unavailable; retry sign-in");
|
|
53
|
+
}
|
|
54
|
+
try { return await response.json(); } catch { throw new Error("invalid sign-in response"); }
|
|
55
|
+
}
|
|
56
|
+
const start = await request("start", { clientKind, publicKeyJwk: key.publicJwk });
|
|
57
|
+
let uri;
|
|
58
|
+
try { uri = new URL(start.verificationUri); } catch { throw new Error("invalid sign-in response"); }
|
|
59
|
+
if (typeof start.deviceCode !== "string" || !/^[A-Za-z0-9_-]{32,128}$/.test(start.deviceCode) ||
|
|
60
|
+
typeof start.userCode !== "string" || !/^[A-Za-z0-9-]{4,32}$/.test(start.userCode) ||
|
|
61
|
+
uri.origin !== base.origin || uri.username || uri.password || uri.pathname !== "/console/connect" ||
|
|
62
|
+
uri.hash || uri.search !== `?code=${start.userCode}` ||
|
|
63
|
+
!Number.isFinite(start.expiresIn) || start.expiresIn <= 0 || start.expiresIn > 1800 ||
|
|
64
|
+
!Number.isFinite(start.interval) || start.interval < 1 || start.interval > 60) {
|
|
65
|
+
throw new Error("invalid sign-in response");
|
|
66
|
+
}
|
|
67
|
+
const deadline = now() + start.expiresIn * 1000;
|
|
68
|
+
output.write(`Sign in and confirm this device: ${uri.href}\nCode: ${start.userCode}\n`);
|
|
69
|
+
try { await openBrowser(uri.href); } catch { /* URL is available for manual opening. */ }
|
|
70
|
+
while (now() < deadline) {
|
|
71
|
+
await sleep(start.interval * 1000);
|
|
72
|
+
if (now() >= deadline) break;
|
|
73
|
+
const token = await request("token", { deviceCode: start.deviceCode });
|
|
74
|
+
if (token.status === "pending") continue;
|
|
75
|
+
if (token.status === "expired") throw new Error("sign-in expired; retry sign-in");
|
|
76
|
+
if (token.status === "denied") throw new Error("sign-in denied; retry sign-in");
|
|
77
|
+
if (token.status !== "approved" || token.clientKind !== clientKind ||
|
|
78
|
+
typeof token.claim !== "string" || !/^hsc_[A-Za-z0-9_-]{43}$/.test(token.claim) ||
|
|
79
|
+
typeof token.organization !== "string" || !token.organization.trim()) throw new Error("invalid sign-in response");
|
|
80
|
+
return { claim: token.claim, organization: token.organization.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").trim().slice(0, 100) };
|
|
81
|
+
}
|
|
82
|
+
throw new Error("sign-in expired; retry sign-in");
|
|
83
|
+
}
|
package/src/host-hook.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
rankedRecallBlocks,
|
|
10
10
|
} from "./session.mjs";
|
|
11
11
|
import { defaultRuntimeDirectory } from "./storage.mjs";
|
|
12
|
-
import { readHookInput,
|
|
12
|
+
import { readHookInput, syncConfigurationAtSessionStart } from "./claude-hook.mjs";
|
|
13
13
|
import { syncManagedDelivery } from "./delivery-sync.mjs";
|
|
14
14
|
import { transcriptDriverFor } from "./transcript-drivers/index.mjs";
|
|
15
15
|
|
|
@@ -161,9 +161,8 @@ export async function runHostLifecycleHook(connection, eventName, {
|
|
|
161
161
|
const runtime = runtimeFactory(connection, { root, captureGeneration: capture.generation });
|
|
162
162
|
if (capture.paused) {
|
|
163
163
|
if (START_EVENTS.has(eventName)) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
await runtime.syncInstructions?.().catch(() => {});
|
|
164
|
+
// Capture pause does not suspend instruction or reference delivery.
|
|
165
|
+
await syncConfigurationAtSessionStart(runtime, connection, root, stderr, { home, env });
|
|
167
166
|
await runtime.heartbeat(connection.capabilities || {});
|
|
168
167
|
} else if (USER_EVENTS.has(eventName)) {
|
|
169
168
|
const delivery = await syncManagedDelivery({ connection, transport: runtime.transport, root, home, env, stderr, phase: "turn" });
|
|
@@ -175,9 +174,7 @@ export async function runHostLifecycleHook(connection, eventName, {
|
|
|
175
174
|
|
|
176
175
|
if (START_EVENTS.has(eventName)) {
|
|
177
176
|
// Refresh/withdraw local context even when replay or heartbeat fails.
|
|
178
|
-
await
|
|
179
|
-
// Instructions have their own brownout boundary, independent of recall.
|
|
180
|
-
await runtime.syncInstructions?.().catch(() => {});
|
|
177
|
+
await syncConfigurationAtSessionStart(runtime, connection, root, stderr, { home, env });
|
|
181
178
|
await runtime.replay();
|
|
182
179
|
await runtime.heartbeat(connection.capabilities || {});
|
|
183
180
|
if (RECALL_INJECTION_ENABLED) {
|
package/src/install.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { chmod, cp, rename, rm, unlink } from "node:fs/promises";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { generateInstallationKeyPair } from "./crypto.mjs";
|
|
6
6
|
import { ConnectionStore, defaultRuntimeDirectory, ensurePrivateDirectory, readJson, writePrivateFile } from "./storage.mjs";
|
|
7
|
-
import { syncAgentInstructions } from "./instructions.mjs";
|
|
7
|
+
import { INSTALL_INSTRUCTION_SYNC_TIMEOUT_MS, syncAgentInstructions } from "./instructions.mjs";
|
|
8
8
|
import { SignedRuntimeTransport } from "./transport.mjs";
|
|
9
9
|
import { inspectCaptureHealth } from "./health.mjs";
|
|
10
10
|
import { INSTALLER_VERSION, RUNTIME_VERSION } from "./version.mjs";
|
|
@@ -232,7 +232,7 @@ export async function installLocalConnection({
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
const instructions = instructionProfile
|
|
235
|
-
? await syncAgentInstructions(connection, { root,
|
|
235
|
+
? await syncAgentInstructions(connection, { root, fetchImpl, timeoutMs: INSTALL_INSTRUCTION_SYNC_TIMEOUT_MS })
|
|
236
236
|
: undefined;
|
|
237
237
|
return {
|
|
238
238
|
installationId,
|
package/src/installer-cli.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enrollWithBrowser } from "./enrollment.mjs";
|
|
1
2
|
import { recordHealthTargets } from "./health.mjs";
|
|
2
3
|
import { spawnSync } from "node:child_process";
|
|
3
4
|
import { existsSync, readdirSync } from "node:fs";
|
|
@@ -78,9 +79,9 @@ export function parseInstallerArgs(argv) {
|
|
|
78
79
|
}
|
|
79
80
|
const serverUrl = values.get("--server");
|
|
80
81
|
const claim = values.get("--claim");
|
|
81
|
-
if (
|
|
82
|
-
!/^hsc_[A-Za-z0-9_-]{43}$/.test(claim)) {
|
|
83
|
-
throw new Error("Usage: agent-connect install <supported-client> --server <https-url> --claim <one-use-claim>");
|
|
82
|
+
if (!["install", "login"].includes(command) || !CLIENT_KINDS.includes(clientKind) || !serverUrl ||
|
|
83
|
+
(claim !== undefined && !/^hsc_[A-Za-z0-9_-]{43}$/.test(claim))) {
|
|
84
|
+
throw new Error("Usage: agent-connect install|login <supported-client> --server <https-url> [--claim <one-use-claim>]");
|
|
84
85
|
}
|
|
85
86
|
return {
|
|
86
87
|
mode: "single",
|
|
@@ -426,6 +427,9 @@ export async function runInstaller(argv, {
|
|
|
426
427
|
sourceRoot,
|
|
427
428
|
claudeConfigPath,
|
|
428
429
|
skillsHome,
|
|
430
|
+
openBrowser,
|
|
431
|
+
enrollmentSleep,
|
|
432
|
+
enrollmentNow,
|
|
429
433
|
} = {}) {
|
|
430
434
|
const input = parseInstallerArgs(argv);
|
|
431
435
|
if (input.mode === "all") {
|
|
@@ -435,7 +439,13 @@ export async function runInstaller(argv, {
|
|
|
435
439
|
});
|
|
436
440
|
}
|
|
437
441
|
const clientVersion = input.clientKind === "claude-code" ? await detectClaude() : await detectHost(input.clientKind);
|
|
438
|
-
const
|
|
442
|
+
const enrollment = !input.claim ? await enrollWithBrowser({
|
|
443
|
+
serverUrl: input.serverUrl, clientKind: input.clientKind, root, fetchImpl, output,
|
|
444
|
+
...(openBrowser ? { openBrowser } : {}), ...(enrollmentSleep ? { sleep: enrollmentSleep } : {}),
|
|
445
|
+
...(enrollmentNow ? { now: enrollmentNow } : {}),
|
|
446
|
+
}) : null;
|
|
447
|
+
if (enrollment) input.claim = enrollment.claim;
|
|
448
|
+
const claimDisclosure = enrollment || await fetchClaimDisclosure({
|
|
439
449
|
serverUrl: input.serverUrl,
|
|
440
450
|
claim: input.claim,
|
|
441
451
|
fetchImpl,
|
package/src/instructions.mjs
CHANGED
|
@@ -7,6 +7,9 @@ import { isActiveInstallation } from "./active.mjs";
|
|
|
7
7
|
import { ConnectionStore, readJson } from "./storage.mjs";
|
|
8
8
|
import { SignedRuntimeTransport } from "./transport.mjs";
|
|
9
9
|
|
|
10
|
+
export const INSTRUCTION_SYNC_TIMEOUT_MS = 8_000;
|
|
11
|
+
export const INSTALL_INSTRUCTION_SYNC_TIMEOUT_MS = 15_000;
|
|
12
|
+
|
|
10
13
|
const sha = (value) => createHash("sha256").update(value).digest("hex");
|
|
11
14
|
const HEX = /^[a-f0-9]{64}$/;
|
|
12
15
|
const MARKER = "<!-- HALOFY-INSTRUCTIONS:";
|
|
@@ -247,20 +250,32 @@ async function retirePredecessorInstructions(connection, root) {
|
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
252
|
|
|
250
|
-
/** Separate best-effort operation: a failed GET never means remove instructions.
|
|
251
|
-
|
|
253
|
+
/** Separate best-effort operation: a failed GET never means remove instructions.
|
|
254
|
+
* GET and receipt share one deadline so a slow receipt cannot double startup delay.
|
|
255
|
+
* An explicitly injected transport retains its own cancellation policy. */
|
|
256
|
+
export async function syncAgentInstructions(connection, {
|
|
257
|
+
root, transport, fetchImpl = globalThis.fetch, timeoutMs = INSTRUCTION_SYNC_TIMEOUT_MS,
|
|
258
|
+
} = {}) {
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
261
|
+
const activeTransport = transport ?? new SignedRuntimeTransport(connection, {
|
|
262
|
+
fetchImpl, timeoutMs, signal: controller.signal,
|
|
263
|
+
});
|
|
252
264
|
let receipt = { digest: sha("[]"), status: "unavailable", reason: "fetch_unavailable" };
|
|
253
265
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
+
try {
|
|
267
|
+
if (!(await isActiveInstallation(connection, root))) return { ...receipt, status: "unsupported", reason: "inactive_installation" };
|
|
268
|
+
await retirePredecessorInstructions(connection, root);
|
|
269
|
+
const bundle = await activeTransport.instructions();
|
|
270
|
+
if (!transport) controller.signal.throwIfAborted();
|
|
271
|
+
receipt.digest = typeof bundle?.digest === "string" && HEX.test(bundle.digest) ? bundle.digest : receipt.digest;
|
|
272
|
+
receipt = await applyInstructionBundle(connection, bundle, { root, canActivate: () => isActiveInstallation(connection, root) });
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const reason = error.instructionCode;
|
|
275
|
+
const unsupported = ["custom_context_unsupported", "reinstall_required", "inactive_installation"].includes(reason);
|
|
276
|
+
receipt = { ...receipt, status: unsupported ? "unsupported" : reason && reason !== "invalid_bundle" ? "conflict" : "unavailable", reason: reason || "fetch_unavailable" };
|
|
277
|
+
}
|
|
278
|
+
try { await activeTransport.instructionStatus(receipt); } catch { /* A receipt failure cannot disable capture/MCP. */ }
|
|
279
|
+
return receipt;
|
|
280
|
+
} finally { clearTimeout(timer); }
|
|
266
281
|
}
|
package/src/runtime.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export class LifecycleRuntime {
|
|
|
16
16
|
constructor(connection, {
|
|
17
17
|
root,
|
|
18
18
|
transport = new SignedRuntimeTransport(connection),
|
|
19
|
-
instructionTransport
|
|
19
|
+
instructionTransport,
|
|
20
20
|
maxBatchEvents = 100,
|
|
21
21
|
maxBatchBytes = 1024 * 1024,
|
|
22
22
|
commitThreshold = 20,
|
package/src/version.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export const PACKAGE_NAME = "@halofy/agent-connect";
|
|
2
|
-
export const INSTALLER_VERSION = "0.
|
|
3
|
-
export const RUNTIME_VERSION = "0.
|
|
2
|
+
export const INSTALLER_VERSION = "0.12.1";
|
|
3
|
+
export const RUNTIME_VERSION = "0.12.1";
|
|
4
4
|
export const DISCLOSURE_VERSION = "halofy-agent-lifecycle-2026-09-10.1";
|