@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.100
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/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +92 -0
- package/README.md +101 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/hellolynk-client-pack-manifest.json +619 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/package.json +58 -4
- package/plugins/hellolynk-codex/.app.json +3 -0
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/hellolynk-codex/.mcp.json +19 -0
- package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/package.json +11 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +277 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
- package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
- package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
- package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
- package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
- package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
- package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
- package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
- package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
- package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
- package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
- package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
6
|
+
|
|
7
|
+
const { sanitizeHelloLynkChildEnvironment } = runtimeSafety;
|
|
8
|
+
|
|
9
|
+
export const HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_CONTRACT =
|
|
10
|
+
"hellolynk-native-human-confirmation-v1";
|
|
11
|
+
export const HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_LEDGER_CONTRACT =
|
|
12
|
+
"hellolynk-native-human-confirmation-ledger-v1";
|
|
13
|
+
export const HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_TTL_MS = 120_000;
|
|
14
|
+
export const HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_RATE_LIMIT = 3;
|
|
15
|
+
export const HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_RATE_WINDOW_MS = 60_000;
|
|
16
|
+
|
|
17
|
+
const RESPONSE_KEYS = Object.freeze([
|
|
18
|
+
"actionId", "adapterId", "adapterInstanceId", "clientMessageId", "contract",
|
|
19
|
+
"createdAtMs", "decision", "delegateId", "expiresAtMs", "hostGeneration",
|
|
20
|
+
"hostInstallationId", "principalId", "proof", "recipientHelloLynkId", "textDigest",
|
|
21
|
+
]);
|
|
22
|
+
const RECIPIENT_INSTANCE_KEYS = Object.freeze(["recipientDelegateId", "recipientInstanceDisplayName"]);
|
|
23
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,198}$/;
|
|
24
|
+
const HELLOLYNK_ID_PATTERN = /^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$/;
|
|
25
|
+
const HEX_64_PATTERN = /^[a-f0-9]{64}$/;
|
|
26
|
+
const MAX_TEXT_BYTES = 20_480;
|
|
27
|
+
// JSON can escape each body byte as six ASCII bytes; allow the fixed metadata too.
|
|
28
|
+
const MAX_CHILD_INPUT_BYTES = MAX_TEXT_BYTES * 6 + 8_192;
|
|
29
|
+
const MAX_CHILD_STDOUT_BYTES = 8_192;
|
|
30
|
+
const MAX_CHILD_STDERR_BYTES = 4_096;
|
|
31
|
+
const POWERSHELL_SCRIPT = fileURLToPath(new URL(
|
|
32
|
+
"../platform/windows/hellolynk-native-human-confirmation-broker.ps1",
|
|
33
|
+
import.meta.url,
|
|
34
|
+
));
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Threat boundary: this broker separates an ordinary adapter/model request from a
|
|
38
|
+
* human decision made in a Host-owned native child process. It does not defend
|
|
39
|
+
* against arbitrary malicious code already executing as the same Windows user.
|
|
40
|
+
*
|
|
41
|
+
* The injected ledger is deliberately narrow. It may durably retain only the
|
|
42
|
+
* sanitized admission/tombstone records passed here. Message text, challenge,
|
|
43
|
+
* HMAC key and proof never cross that interface.
|
|
44
|
+
*/
|
|
45
|
+
export function createHelloLynkNativeHumanConfirmationBroker(options = {}) {
|
|
46
|
+
const ledger = assertLedger(options.ledger);
|
|
47
|
+
const launcher = options.launcher || createWindowsNativeHumanConfirmationLauncher();
|
|
48
|
+
const onApproved = typeof options.onApproved === "function"
|
|
49
|
+
? options.onApproved
|
|
50
|
+
: async () => { throw codeError("human_confirmation_approval_handler_missing", 500); };
|
|
51
|
+
const clock = normalizeClock(options.clock);
|
|
52
|
+
const scheduler = normalizeScheduler(options.scheduler);
|
|
53
|
+
const randomBytes = typeof options.randomBytes === "function" ? options.randomBytes : crypto.randomBytes;
|
|
54
|
+
const approvalDrainTimeoutMs = normalizeDrainTimeout(options.approvalDrainTimeoutMs);
|
|
55
|
+
let hostGeneration = assertIdentifier(
|
|
56
|
+
options.hostGeneration,
|
|
57
|
+
"human_confirmation_host_generation_invalid",
|
|
58
|
+
);
|
|
59
|
+
let closed = false;
|
|
60
|
+
let gate = Promise.resolve();
|
|
61
|
+
const actions = new Map();
|
|
62
|
+
const pendingByPartition = new Map();
|
|
63
|
+
const approvalTasks = new Map();
|
|
64
|
+
const settlementTasks = new Set();
|
|
65
|
+
let closePromise = null;
|
|
66
|
+
|
|
67
|
+
function locked(operation) {
|
|
68
|
+
const next = gate.then(operation, operation);
|
|
69
|
+
gate = next.catch(() => {});
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function request(value, context = {}) {
|
|
74
|
+
const normalized = normalizeRequest(value, hostGeneration);
|
|
75
|
+
const nativeAuthority = context.nativeAuthority;
|
|
76
|
+
if (nativeAuthority != null && (typeof nativeAuthority.assertCurrent !== "function"
|
|
77
|
+
|| typeof nativeAuthority.revalidate !== "function")) {
|
|
78
|
+
throw codeError("human_confirmation_authority_invalidated", 409);
|
|
79
|
+
}
|
|
80
|
+
assertNativeAuthority(nativeAuthority);
|
|
81
|
+
const nowMs = assertNow(clock.now());
|
|
82
|
+
const partitionHash = hashParts(normalized.capabilityId, normalized.adapterInstanceId);
|
|
83
|
+
const requestHash = hashParts(
|
|
84
|
+
normalized.principalId,
|
|
85
|
+
normalized.delegateId,
|
|
86
|
+
normalized.hostInstallationId,
|
|
87
|
+
normalized.adapterId,
|
|
88
|
+
normalized.adapterInstanceId,
|
|
89
|
+
normalized.hostGeneration,
|
|
90
|
+
normalized.recipientHelloLynkId,
|
|
91
|
+
...(Object.hasOwn(normalized, "recipientDelegateId") ? [normalized.recipientDelegateId] : []),
|
|
92
|
+
normalized.clientMessageId,
|
|
93
|
+
normalized.textDigest,
|
|
94
|
+
);
|
|
95
|
+
let launchRecord;
|
|
96
|
+
const response = await locked(async () => {
|
|
97
|
+
if (closed) throw codeError("human_confirmation_broker_closed", 503);
|
|
98
|
+
assertNativeAuthority(nativeAuthority);
|
|
99
|
+
if (normalized.hostGeneration !== hostGeneration) {
|
|
100
|
+
throw codeError("human_confirmation_host_generation_mismatch", 409);
|
|
101
|
+
}
|
|
102
|
+
const currentId = pendingByPartition.get(partitionHash);
|
|
103
|
+
const current = currentId ? actions.get(currentId) : null;
|
|
104
|
+
if (current && current.state === "pending" && current.expiresAtMs <= nowMs) {
|
|
105
|
+
await terminalizeLocked(current, "expired", "human_confirmation_timed_out", nowMs);
|
|
106
|
+
}
|
|
107
|
+
assertNativeAuthority(nativeAuthority);
|
|
108
|
+
// Keep the selected stable peer attached to this client message after the
|
|
109
|
+
// dialog settles too. A result whose delivery may be unknown is queried,
|
|
110
|
+
// never turned into a second human approval or a different peer.
|
|
111
|
+
for (const previous of actions.values()) {
|
|
112
|
+
if (previous.partitionHash !== partitionHash || previous.clientMessageId !== normalized.clientMessageId
|
|
113
|
+
|| (!Object.hasOwn(previous, "recipientDelegateId") && !Object.hasOwn(normalized, "recipientDelegateId"))) continue;
|
|
114
|
+
if (previous.requestHash !== requestHash) throw codeError("human_confirmation_message_conflict", 409);
|
|
115
|
+
return publicStatus(previous, ["pending", "consuming"].includes(previous.state) ? 202 : 200, true);
|
|
116
|
+
}
|
|
117
|
+
const stillPendingId = pendingByPartition.get(partitionHash);
|
|
118
|
+
const stillPending = stillPendingId ? actions.get(stillPendingId) : null;
|
|
119
|
+
if (stillPending?.state === "pending") {
|
|
120
|
+
if (stillPending.requestHash === requestHash) return publicStatus(stillPending, 202, true);
|
|
121
|
+
throw codeError("human_confirmation_pending_conflict", 409);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const actionId = `human_${randomToken(randomBytes, 24)}`;
|
|
125
|
+
const createdAtMs = nowMs;
|
|
126
|
+
const expiresAtMs = createdAtMs + HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_TTL_MS;
|
|
127
|
+
const admission = await ledger.admit(Object.freeze({
|
|
128
|
+
contract: HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_LEDGER_CONTRACT,
|
|
129
|
+
actionId,
|
|
130
|
+
partitionHash,
|
|
131
|
+
createdAtMs,
|
|
132
|
+
expiresAtMs,
|
|
133
|
+
rateWindowMs: HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_RATE_WINDOW_MS,
|
|
134
|
+
rateLimit: HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_RATE_LIMIT,
|
|
135
|
+
restartTombstoneState: "invalidated",
|
|
136
|
+
restartTombstoneCode: "human_confirmation_restart_invalidated",
|
|
137
|
+
}));
|
|
138
|
+
if (!admission || admission.accepted !== true) {
|
|
139
|
+
if (admission?.reason === "rate_limited") {
|
|
140
|
+
throw codeError("human_confirmation_rate_limited", 429, admission.retryAfterMs);
|
|
141
|
+
}
|
|
142
|
+
if (admission?.reason === "partition_pending") {
|
|
143
|
+
throw codeError("human_confirmation_ledger_unavailable", 503);
|
|
144
|
+
}
|
|
145
|
+
throw codeError("human_confirmation_ledger_unavailable", 503);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Admission may itself be a durable I/O wait. Never launch a dialog from
|
|
149
|
+
// the stale pre-I/O timestamp after the entire action TTL has elapsed.
|
|
150
|
+
try {
|
|
151
|
+
if (nativeAuthority) await nativeAuthority.revalidate();
|
|
152
|
+
assertNativeAuthority(nativeAuthority);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
try {
|
|
155
|
+
await recordTombstone({ actionId, partitionHash, createdAtMs, expiresAtMs },
|
|
156
|
+
"invalidated", "human_confirmation_authority_invalidated", assertNow(clock.now()));
|
|
157
|
+
} catch {
|
|
158
|
+
throw codeError("human_confirmation_ledger_unavailable", 503);
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
const admittedAtMs = assertNow(clock.now());
|
|
163
|
+
if (admittedAtMs >= expiresAtMs) {
|
|
164
|
+
try {
|
|
165
|
+
await ledger.record(Object.freeze({
|
|
166
|
+
contract: HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_LEDGER_CONTRACT,
|
|
167
|
+
actionId,
|
|
168
|
+
partitionHash,
|
|
169
|
+
state: "expired",
|
|
170
|
+
code: "human_confirmation_timed_out",
|
|
171
|
+
createdAtMs,
|
|
172
|
+
expiresAtMs,
|
|
173
|
+
recordedAtMs: admittedAtMs,
|
|
174
|
+
}));
|
|
175
|
+
} catch {
|
|
176
|
+
throw codeError("human_confirmation_ledger_unavailable", 503);
|
|
177
|
+
}
|
|
178
|
+
throw codeError("human_confirmation_timed_out", 408);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const challenge = randomToken(randomBytes, 32);
|
|
182
|
+
const hmacKey = Buffer.from(randomBytes(32));
|
|
183
|
+
if (hmacKey.length !== 32) throw codeError("human_confirmation_random_source_invalid", 500);
|
|
184
|
+
const action = {
|
|
185
|
+
...normalized,
|
|
186
|
+
actionId,
|
|
187
|
+
createdAtMs,
|
|
188
|
+
expiresAtMs,
|
|
189
|
+
partitionHash,
|
|
190
|
+
requestHash,
|
|
191
|
+
challenge,
|
|
192
|
+
hmacKey,
|
|
193
|
+
nativeAuthority,
|
|
194
|
+
state: "pending",
|
|
195
|
+
code: null,
|
|
196
|
+
abortController: new AbortController(),
|
|
197
|
+
timer: null,
|
|
198
|
+
};
|
|
199
|
+
actions.set(actionId, action);
|
|
200
|
+
pendingByPartition.set(partitionHash, actionId);
|
|
201
|
+
action.timer = scheduler.setTimeout(() => {
|
|
202
|
+
void expire(actionId);
|
|
203
|
+
}, expiresAtMs - admittedAtMs);
|
|
204
|
+
launchRecord = action;
|
|
205
|
+
return publicStatus(action, 202, false);
|
|
206
|
+
});
|
|
207
|
+
if (launchRecord) startLauncher(launchRecord);
|
|
208
|
+
return response;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function status(value) {
|
|
212
|
+
const query = normalizeStatusQuery(value);
|
|
213
|
+
const partitionHash = hashParts(query.capabilityId, query.adapterInstanceId);
|
|
214
|
+
return locked(async () => {
|
|
215
|
+
const action = actions.get(query.actionId);
|
|
216
|
+
if (action) {
|
|
217
|
+
if (action.partitionHash !== partitionHash) throw codeError("human_confirmation_not_found", 404);
|
|
218
|
+
if (action.state === "pending" && action.expiresAtMs <= assertNow(clock.now())) {
|
|
219
|
+
await terminalizeLocked(
|
|
220
|
+
action,
|
|
221
|
+
"expired",
|
|
222
|
+
"human_confirmation_timed_out",
|
|
223
|
+
assertNow(clock.now()),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return publicStatus(action, 200, false);
|
|
227
|
+
}
|
|
228
|
+
const tombstone = await ledger.lookup(Object.freeze({
|
|
229
|
+
contract: HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_LEDGER_CONTRACT,
|
|
230
|
+
actionId: query.actionId,
|
|
231
|
+
partitionHash,
|
|
232
|
+
}));
|
|
233
|
+
if (!tombstone) throw codeError("human_confirmation_not_found", 404);
|
|
234
|
+
return publicTombstone(tombstone);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function detachAdapter(value) {
|
|
239
|
+
const query = normalizePartitionQuery(value);
|
|
240
|
+
const partitionHash = hashParts(query.capabilityId, query.adapterInstanceId);
|
|
241
|
+
return locked(async () => {
|
|
242
|
+
const actionId = pendingByPartition.get(partitionHash);
|
|
243
|
+
const action = actionId ? actions.get(actionId) : null;
|
|
244
|
+
if (!action || action.state !== "pending") return Object.freeze({ detached: true, invalidated: 0 });
|
|
245
|
+
await terminalizeLocked(
|
|
246
|
+
action,
|
|
247
|
+
"invalidated",
|
|
248
|
+
"human_confirmation_adapter_detached",
|
|
249
|
+
assertNow(clock.now()),
|
|
250
|
+
);
|
|
251
|
+
return Object.freeze({ detached: true, invalidated: 1 });
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function setHostGeneration(nextGeneration) {
|
|
256
|
+
const normalized = assertIdentifier(
|
|
257
|
+
nextGeneration,
|
|
258
|
+
"human_confirmation_host_generation_invalid",
|
|
259
|
+
);
|
|
260
|
+
return locked(async () => {
|
|
261
|
+
if (closed) throw codeError("human_confirmation_broker_closed", 503);
|
|
262
|
+
if (normalized === hostGeneration) return Object.freeze({ hostGeneration, invalidated: 0 });
|
|
263
|
+
const nowMs = assertNow(clock.now());
|
|
264
|
+
let invalidated = 0;
|
|
265
|
+
for (const action of actions.values()) {
|
|
266
|
+
if (action.state !== "pending") continue;
|
|
267
|
+
await terminalizeLocked(
|
|
268
|
+
action,
|
|
269
|
+
"invalidated",
|
|
270
|
+
"human_confirmation_host_generation_mismatch",
|
|
271
|
+
nowMs,
|
|
272
|
+
);
|
|
273
|
+
invalidated += 1;
|
|
274
|
+
}
|
|
275
|
+
hostGeneration = normalized;
|
|
276
|
+
return Object.freeze({ hostGeneration, invalidated });
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function close() {
|
|
281
|
+
if (closePromise) return closePromise;
|
|
282
|
+
closePromise = (async () => {
|
|
283
|
+
const consuming = await locked(async () => {
|
|
284
|
+
if (!closed) {
|
|
285
|
+
closed = true;
|
|
286
|
+
const nowMs = assertNow(clock.now());
|
|
287
|
+
for (const action of actions.values()) {
|
|
288
|
+
if (action.state !== "pending") continue;
|
|
289
|
+
await terminalizeLocked(
|
|
290
|
+
action,
|
|
291
|
+
"invalidated",
|
|
292
|
+
"human_confirmation_restart_invalidated",
|
|
293
|
+
nowMs,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
for (const action of actions.values()) {
|
|
297
|
+
if (action.state === "consuming") action.approvalAbortController?.abort();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return [...new Set([...settlementTasks, ...approvalTasks.values()])];
|
|
301
|
+
});
|
|
302
|
+
if (consuming.length === 0) return;
|
|
303
|
+
let timedOut = false;
|
|
304
|
+
let timeoutHandle = null;
|
|
305
|
+
const timeout = new Promise((resolve) => {
|
|
306
|
+
timeoutHandle = scheduler.setTimeout(() => { timedOut = true; resolve(); }, approvalDrainTimeoutMs);
|
|
307
|
+
});
|
|
308
|
+
await Promise.race([Promise.allSettled(consuming), timeout]);
|
|
309
|
+
if (!timedOut && timeoutHandle != null) scheduler.clearTimeout(timeoutHandle);
|
|
310
|
+
if (!timedOut) return;
|
|
311
|
+
await locked(async () => {
|
|
312
|
+
for (const action of actions.values()) {
|
|
313
|
+
if (action.state !== "consuming") continue;
|
|
314
|
+
action.state = "failed";
|
|
315
|
+
action.code = "human_confirmation_approval_failed";
|
|
316
|
+
try {
|
|
317
|
+
await recordTombstone(action, action.state, action.code, assertNow(clock.now()));
|
|
318
|
+
} catch {
|
|
319
|
+
action.code = "human_confirmation_ledger_unavailable";
|
|
320
|
+
} finally {
|
|
321
|
+
scrubActionSecrets(action);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
})();
|
|
326
|
+
return closePromise;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function startLauncher(action) {
|
|
330
|
+
Promise.resolve().then(async () => {
|
|
331
|
+
if (action.state !== "pending") return;
|
|
332
|
+
try {
|
|
333
|
+
assertNativeAuthority(action.nativeAuthority);
|
|
334
|
+
} catch {
|
|
335
|
+
await locked(() => terminalizeLocked(action, "invalidated",
|
|
336
|
+
"human_confirmation_authority_invalidated", assertNow(clock.now())));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const input = Object.freeze({
|
|
340
|
+
contract: HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_CONTRACT,
|
|
341
|
+
actionId: action.actionId,
|
|
342
|
+
principalId: action.principalId,
|
|
343
|
+
delegateId: action.delegateId,
|
|
344
|
+
hostInstallationId: action.hostInstallationId,
|
|
345
|
+
adapterId: action.adapterId,
|
|
346
|
+
adapterInstanceId: action.adapterInstanceId,
|
|
347
|
+
hostGeneration: action.hostGeneration,
|
|
348
|
+
recipientHelloLynkId: action.recipientHelloLynkId,
|
|
349
|
+
...recipientInstanceFields(action),
|
|
350
|
+
clientMessageId: action.clientMessageId,
|
|
351
|
+
textDigest: action.textDigest,
|
|
352
|
+
createdAtMs: action.createdAtMs,
|
|
353
|
+
expiresAtMs: action.expiresAtMs,
|
|
354
|
+
challenge: action.challenge,
|
|
355
|
+
hmacKey: action.hmacKey.toString("base64url"),
|
|
356
|
+
displayText: action.text,
|
|
357
|
+
});
|
|
358
|
+
return launcher.run(input, {
|
|
359
|
+
signal: action.abortController.signal,
|
|
360
|
+
deliver: (result) => trackSettlement(action.actionId, result),
|
|
361
|
+
});
|
|
362
|
+
}).catch(() => trackSettlement(action.actionId, {
|
|
363
|
+
kind: "failure",
|
|
364
|
+
code: "human_confirmation_dialog_crashed",
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function trackSettlement(actionId, result) {
|
|
369
|
+
const task = settle(actionId, result);
|
|
370
|
+
settlementTasks.add(task);
|
|
371
|
+
task.finally(() => settlementTasks.delete(task)).catch(() => {});
|
|
372
|
+
return task;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function settle(actionId, childResult) {
|
|
376
|
+
let approved;
|
|
377
|
+
try {
|
|
378
|
+
approved = await locked(async () => {
|
|
379
|
+
const action = actions.get(actionId);
|
|
380
|
+
if (!action || action.state !== "pending") return null;
|
|
381
|
+
try {
|
|
382
|
+
assertNativeAuthority(action.nativeAuthority);
|
|
383
|
+
} catch {
|
|
384
|
+
await terminalizeLocked(action, "invalidated",
|
|
385
|
+
"human_confirmation_authority_invalidated", assertNow(clock.now()));
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const decisionReceivedAtMs = assertNow(clock.now());
|
|
389
|
+
if (decisionReceivedAtMs >= action.expiresAtMs) {
|
|
390
|
+
await terminalizeLocked(
|
|
391
|
+
action,
|
|
392
|
+
"expired",
|
|
393
|
+
"human_confirmation_timed_out",
|
|
394
|
+
decisionReceivedAtMs,
|
|
395
|
+
);
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
if (action.hostGeneration !== hostGeneration) {
|
|
399
|
+
await terminalizeLocked(
|
|
400
|
+
action,
|
|
401
|
+
"invalidated",
|
|
402
|
+
"human_confirmation_host_generation_mismatch",
|
|
403
|
+
assertNow(clock.now()),
|
|
404
|
+
);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
if (childResult?.kind === "failure") {
|
|
408
|
+
await terminalizeLocked(
|
|
409
|
+
action,
|
|
410
|
+
failureState(childResult.code),
|
|
411
|
+
normalizeLauncherFailure(childResult.code),
|
|
412
|
+
assertNow(clock.now()),
|
|
413
|
+
);
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
let response;
|
|
417
|
+
try {
|
|
418
|
+
response = assertChildResponse(childResult?.value, action);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
await terminalizeLocked(
|
|
421
|
+
action,
|
|
422
|
+
"failed",
|
|
423
|
+
safeCode(error, "human_confirmation_response_malformed"),
|
|
424
|
+
assertNow(clock.now()),
|
|
425
|
+
);
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const mismatch = responseBindingMismatch(action, response);
|
|
429
|
+
if (mismatch) {
|
|
430
|
+
await terminalizeLocked(action, "failed", mismatch, assertNow(clock.now()));
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
const expectedProof = createDecisionProof(action, response.decision);
|
|
434
|
+
if (!safeHexEqual(expectedProof, response.proof)) {
|
|
435
|
+
await terminalizeLocked(
|
|
436
|
+
action,
|
|
437
|
+
"failed",
|
|
438
|
+
"human_confirmation_proof_invalid",
|
|
439
|
+
assertNow(clock.now()),
|
|
440
|
+
);
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
if (response.decision === "cancel") {
|
|
444
|
+
await terminalizeLocked(
|
|
445
|
+
action,
|
|
446
|
+
"cancelled",
|
|
447
|
+
"human_confirmation_cancelled",
|
|
448
|
+
assertNow(clock.now()),
|
|
449
|
+
);
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// This state transition is deliberately synchronous and happens before
|
|
454
|
+
// the first await. Concurrent/replayed child deliveries can no longer win.
|
|
455
|
+
action.state = "consuming";
|
|
456
|
+
action.code = null;
|
|
457
|
+
action.approvalAbortController = new AbortController();
|
|
458
|
+
pendingByPartition.delete(action.partitionHash);
|
|
459
|
+
clearActionResources(action);
|
|
460
|
+
try {
|
|
461
|
+
await recordTombstone(action, "consuming", null, assertNow(clock.now()));
|
|
462
|
+
} catch {
|
|
463
|
+
action.state = "failed";
|
|
464
|
+
action.code = "human_confirmation_ledger_unavailable";
|
|
465
|
+
scrubActionSecrets(action);
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
action,
|
|
470
|
+
approval: Object.freeze({
|
|
471
|
+
contract: HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_CONTRACT,
|
|
472
|
+
actionId: action.actionId,
|
|
473
|
+
principalId: action.principalId,
|
|
474
|
+
delegateId: action.delegateId,
|
|
475
|
+
hostInstallationId: action.hostInstallationId,
|
|
476
|
+
adapterId: action.adapterId,
|
|
477
|
+
adapterInstanceId: action.adapterInstanceId,
|
|
478
|
+
hostGeneration: action.hostGeneration,
|
|
479
|
+
recipientHelloLynkId: action.recipientHelloLynkId,
|
|
480
|
+
...recipientInstanceFields(action),
|
|
481
|
+
clientMessageId: action.clientMessageId,
|
|
482
|
+
text: action.text,
|
|
483
|
+
textDigest: action.textDigest,
|
|
484
|
+
intentSource: "human_native_dialog",
|
|
485
|
+
approvedAtMs: assertNow(clock.now()),
|
|
486
|
+
}),
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
} catch {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (!approved) return;
|
|
493
|
+
const task = runApproved(approved);
|
|
494
|
+
approvalTasks.set(actionId, task);
|
|
495
|
+
try { await task; } finally { approvalTasks.delete(actionId); }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function runApproved(approved) {
|
|
499
|
+
let finalState = "approved";
|
|
500
|
+
let finalCode = null;
|
|
501
|
+
try {
|
|
502
|
+
if (approved.action.nativeAuthority) await approved.action.nativeAuthority.revalidate();
|
|
503
|
+
assertNativeAuthority(approved.action.nativeAuthority);
|
|
504
|
+
await onApproved(approved.approval, {
|
|
505
|
+
signal: approved.action.approvalAbortController.signal,
|
|
506
|
+
nativeAuthority: approved.action.nativeAuthority,
|
|
507
|
+
});
|
|
508
|
+
} catch {
|
|
509
|
+
finalState = "failed";
|
|
510
|
+
finalCode = "human_confirmation_approval_failed";
|
|
511
|
+
}
|
|
512
|
+
await locked(async () => {
|
|
513
|
+
const { action } = approved;
|
|
514
|
+
if (action.state !== "consuming") return;
|
|
515
|
+
action.state = finalState;
|
|
516
|
+
action.code = finalCode;
|
|
517
|
+
try {
|
|
518
|
+
await recordTombstone(action, finalState, finalCode, assertNow(clock.now()));
|
|
519
|
+
} catch {
|
|
520
|
+
action.state = "failed";
|
|
521
|
+
action.code = "human_confirmation_ledger_unavailable";
|
|
522
|
+
} finally {
|
|
523
|
+
scrubActionSecrets(action);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function expire(actionId) {
|
|
529
|
+
await locked(async () => {
|
|
530
|
+
const action = actions.get(actionId);
|
|
531
|
+
if (!action || action.state !== "pending") return;
|
|
532
|
+
await terminalizeLocked(
|
|
533
|
+
action,
|
|
534
|
+
"expired",
|
|
535
|
+
"human_confirmation_timed_out",
|
|
536
|
+
assertNow(clock.now()),
|
|
537
|
+
);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function terminalizeLocked(action, state, code, nowMs) {
|
|
542
|
+
if (action.state !== "pending") return;
|
|
543
|
+
action.state = state;
|
|
544
|
+
action.code = code;
|
|
545
|
+
pendingByPartition.delete(action.partitionHash);
|
|
546
|
+
clearActionResources(action);
|
|
547
|
+
try {
|
|
548
|
+
await recordTombstone(action, state, code, nowMs);
|
|
549
|
+
} catch {
|
|
550
|
+
action.state = "failed";
|
|
551
|
+
action.code = "human_confirmation_ledger_unavailable";
|
|
552
|
+
} finally {
|
|
553
|
+
scrubActionSecrets(action);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function recordTombstone(action, state, code, recordedAtMs) {
|
|
558
|
+
await ledger.record(Object.freeze({
|
|
559
|
+
contract: HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_LEDGER_CONTRACT,
|
|
560
|
+
actionId: action.actionId,
|
|
561
|
+
partitionHash: action.partitionHash,
|
|
562
|
+
state,
|
|
563
|
+
code,
|
|
564
|
+
createdAtMs: action.createdAtMs,
|
|
565
|
+
expiresAtMs: action.expiresAtMs,
|
|
566
|
+
recordedAtMs,
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function clearActionResources(action) {
|
|
571
|
+
if (action.timer != null) scheduler.clearTimeout(action.timer);
|
|
572
|
+
action.timer = null;
|
|
573
|
+
action.abortController.abort();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return Object.freeze({ request, status, detachAdapter, setHostGeneration, close });
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function createWindowsNativeHumanConfirmationLauncher() {
|
|
580
|
+
return Object.freeze({
|
|
581
|
+
async run(input, context = {}) {
|
|
582
|
+
if (context.signal?.aborted) {
|
|
583
|
+
await context.deliver?.({ kind: "failure", code: "human_confirmation_timed_out" });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (process.platform !== "win32") {
|
|
587
|
+
await context.deliver?.({ kind: "failure", code: "human_confirmation_platform_unsupported" });
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const serialized = JSON.stringify(input);
|
|
591
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_CHILD_INPUT_BYTES) {
|
|
592
|
+
await context.deliver?.({ kind: "failure", code: "human_confirmation_request_too_large" });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
await runPowerShellChild(serialized, context);
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function runPowerShellChild(serialized, context) {
|
|
601
|
+
await new Promise((resolve) => {
|
|
602
|
+
let delivered = false;
|
|
603
|
+
let stdout = "";
|
|
604
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
605
|
+
let stdoutBytes = 0;
|
|
606
|
+
let stderrBytes = 0;
|
|
607
|
+
let forcedCode = null;
|
|
608
|
+
const child = spawn("powershell.exe", [
|
|
609
|
+
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
610
|
+
"-STA", "-File", POWERSHELL_SCRIPT,
|
|
611
|
+
], {
|
|
612
|
+
windowsHide: true,
|
|
613
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
614
|
+
env: sanitizeHelloLynkChildEnvironment(process.env),
|
|
615
|
+
});
|
|
616
|
+
const finish = async (result) => {
|
|
617
|
+
if (delivered) return;
|
|
618
|
+
delivered = true;
|
|
619
|
+
try { await context.deliver?.(result); } finally { resolve(); }
|
|
620
|
+
};
|
|
621
|
+
const abort = () => {
|
|
622
|
+
forcedCode ||= "human_confirmation_timed_out";
|
|
623
|
+
child.kill();
|
|
624
|
+
};
|
|
625
|
+
if (context.signal?.aborted) abort();
|
|
626
|
+
else context.signal?.addEventListener("abort", abort, { once: true });
|
|
627
|
+
child.once("error", () => void finish({
|
|
628
|
+
kind: "failure",
|
|
629
|
+
code: "human_confirmation_dialog_crashed",
|
|
630
|
+
}));
|
|
631
|
+
child.stdout.on("data", (chunk) => {
|
|
632
|
+
stdoutBytes += chunk.length;
|
|
633
|
+
if (stdoutBytes > MAX_CHILD_STDOUT_BYTES) {
|
|
634
|
+
forcedCode = "human_confirmation_stdout_oversize";
|
|
635
|
+
child.kill();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
stdout += stdoutDecoder.write(chunk);
|
|
639
|
+
});
|
|
640
|
+
child.stderr.on("data", (chunk) => {
|
|
641
|
+
stderrBytes += chunk.length;
|
|
642
|
+
if (stderrBytes > MAX_CHILD_STDERR_BYTES) {
|
|
643
|
+
forcedCode = "human_confirmation_stderr_oversize";
|
|
644
|
+
child.kill();
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
child.once("close", (code) => {
|
|
648
|
+
context.signal?.removeEventListener("abort", abort);
|
|
649
|
+
if (delivered) return;
|
|
650
|
+
if (forcedCode) return void finish({ kind: "failure", code: forcedCode });
|
|
651
|
+
if (code !== 0) return void finish({
|
|
652
|
+
kind: "failure",
|
|
653
|
+
code: "human_confirmation_dialog_crashed",
|
|
654
|
+
});
|
|
655
|
+
stdout += stdoutDecoder.end();
|
|
656
|
+
if (!/^[^\r\n]+(?:\r?\n)?$/.test(stdout)) {
|
|
657
|
+
return void finish({ kind: "failure", code: "human_confirmation_stdout_extra" });
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
const value = JSON.parse(stdout.trim());
|
|
661
|
+
return void finish({ kind: "response", value });
|
|
662
|
+
} catch {
|
|
663
|
+
return void finish({ kind: "failure", code: "human_confirmation_response_malformed" });
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
child.stdin.once("error", () => {});
|
|
667
|
+
child.stdin.end(serialized, "utf8");
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function normalizeRequest(value, expectedGeneration) {
|
|
672
|
+
assertPlainObject(value, "human_confirmation_request_invalid");
|
|
673
|
+
// Legacy standalone callers keep their exact old shape. The formal Host
|
|
674
|
+
// requires both peer fields before entering this internal boundary.
|
|
675
|
+
const hasRecipientInstance = RECIPIENT_INSTANCE_KEYS.some((key) => Object.hasOwn(value, key));
|
|
676
|
+
assertExactKeys(value, [
|
|
677
|
+
"adapterId", "adapterInstanceId", "capabilityId", "clientMessageId", "delegateId",
|
|
678
|
+
"hostGeneration", "hostInstallationId", "principalId", "recipientHelloLynkId", "text",
|
|
679
|
+
...(hasRecipientInstance ? RECIPIENT_INSTANCE_KEYS : []),
|
|
680
|
+
], "human_confirmation_request_invalid");
|
|
681
|
+
const text = assertText(value.text);
|
|
682
|
+
const normalized = {
|
|
683
|
+
capabilityId: assertIdentifier(value.capabilityId, "human_confirmation_capability_id_invalid"),
|
|
684
|
+
principalId: assertIdentifier(value.principalId, "human_confirmation_principal_id_invalid"),
|
|
685
|
+
delegateId: assertIdentifier(value.delegateId, "human_confirmation_delegate_id_invalid"),
|
|
686
|
+
hostInstallationId: assertIdentifier(
|
|
687
|
+
value.hostInstallationId,
|
|
688
|
+
"human_confirmation_host_installation_id_invalid",
|
|
689
|
+
),
|
|
690
|
+
adapterId: assertIdentifier(value.adapterId, "human_confirmation_adapter_id_invalid"),
|
|
691
|
+
adapterInstanceId: assertIdentifier(
|
|
692
|
+
value.adapterInstanceId,
|
|
693
|
+
"human_confirmation_adapter_instance_id_invalid",
|
|
694
|
+
),
|
|
695
|
+
hostGeneration: assertIdentifier(
|
|
696
|
+
value.hostGeneration,
|
|
697
|
+
"human_confirmation_host_generation_invalid",
|
|
698
|
+
),
|
|
699
|
+
recipientHelloLynkId: assertHelloLynkId(value.recipientHelloLynkId),
|
|
700
|
+
...(hasRecipientInstance ? {
|
|
701
|
+
recipientDelegateId: assertIdentifier(value.recipientDelegateId, "human_confirmation_recipient_delegate_id_invalid"),
|
|
702
|
+
recipientInstanceDisplayName: assertInstanceDisplayName(value.recipientInstanceDisplayName),
|
|
703
|
+
} : {}),
|
|
704
|
+
clientMessageId: assertIdentifier(
|
|
705
|
+
value.clientMessageId,
|
|
706
|
+
"human_confirmation_client_message_id_invalid",
|
|
707
|
+
),
|
|
708
|
+
text,
|
|
709
|
+
textDigest: crypto.createHash("sha256").update(text, "utf8").digest("hex"),
|
|
710
|
+
};
|
|
711
|
+
if (normalized.hostGeneration !== expectedGeneration) {
|
|
712
|
+
throw codeError("human_confirmation_host_generation_mismatch", 409);
|
|
713
|
+
}
|
|
714
|
+
return Object.freeze(normalized);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function normalizeStatusQuery(value) {
|
|
718
|
+
assertPlainObject(value, "human_confirmation_status_query_invalid");
|
|
719
|
+
assertExactKeys(
|
|
720
|
+
value,
|
|
721
|
+
["actionId", "adapterInstanceId", "capabilityId"],
|
|
722
|
+
"human_confirmation_status_query_invalid",
|
|
723
|
+
);
|
|
724
|
+
return Object.freeze({
|
|
725
|
+
actionId: assertIdentifier(value.actionId, "human_confirmation_action_id_invalid"),
|
|
726
|
+
capabilityId: assertIdentifier(value.capabilityId, "human_confirmation_capability_id_invalid"),
|
|
727
|
+
adapterInstanceId: assertIdentifier(
|
|
728
|
+
value.adapterInstanceId,
|
|
729
|
+
"human_confirmation_adapter_instance_id_invalid",
|
|
730
|
+
),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function normalizePartitionQuery(value) {
|
|
735
|
+
assertPlainObject(value, "human_confirmation_adapter_query_invalid");
|
|
736
|
+
assertExactKeys(
|
|
737
|
+
value,
|
|
738
|
+
["adapterInstanceId", "capabilityId"],
|
|
739
|
+
"human_confirmation_adapter_query_invalid",
|
|
740
|
+
);
|
|
741
|
+
return Object.freeze({
|
|
742
|
+
capabilityId: assertIdentifier(value.capabilityId, "human_confirmation_capability_id_invalid"),
|
|
743
|
+
adapterInstanceId: assertIdentifier(
|
|
744
|
+
value.adapterInstanceId,
|
|
745
|
+
"human_confirmation_adapter_instance_id_invalid",
|
|
746
|
+
),
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function assertChildResponse(value, action) {
|
|
751
|
+
assertPlainObject(value, "human_confirmation_response_malformed");
|
|
752
|
+
const hasRecipientInstance = Object.hasOwn(action, "recipientDelegateId");
|
|
753
|
+
assertExactKeys(value, [...RESPONSE_KEYS, ...(hasRecipientInstance ? RECIPIENT_INSTANCE_KEYS : [])], "human_confirmation_response_malformed");
|
|
754
|
+
if (value.contract !== HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_CONTRACT) {
|
|
755
|
+
throw codeError("human_confirmation_response_malformed", 500);
|
|
756
|
+
}
|
|
757
|
+
if (value.decision !== "approve" && value.decision !== "cancel") {
|
|
758
|
+
throw codeError("human_confirmation_response_malformed", 500);
|
|
759
|
+
}
|
|
760
|
+
const normalized = {
|
|
761
|
+
contract: value.contract,
|
|
762
|
+
actionId: assertIdentifier(value.actionId, "human_confirmation_response_malformed"),
|
|
763
|
+
decision: value.decision,
|
|
764
|
+
principalId: assertIdentifier(value.principalId, "human_confirmation_response_malformed"),
|
|
765
|
+
delegateId: assertIdentifier(value.delegateId, "human_confirmation_response_malformed"),
|
|
766
|
+
hostInstallationId: assertIdentifier(value.hostInstallationId, "human_confirmation_response_malformed"),
|
|
767
|
+
adapterId: assertIdentifier(value.adapterId, "human_confirmation_response_malformed"),
|
|
768
|
+
adapterInstanceId: assertIdentifier(value.adapterInstanceId, "human_confirmation_response_malformed"),
|
|
769
|
+
hostGeneration: assertIdentifier(value.hostGeneration, "human_confirmation_response_malformed"),
|
|
770
|
+
recipientHelloLynkId: assertHelloLynkId(value.recipientHelloLynkId, "human_confirmation_response_malformed"),
|
|
771
|
+
...(hasRecipientInstance ? {
|
|
772
|
+
recipientDelegateId: assertIdentifier(value.recipientDelegateId, "human_confirmation_response_malformed"),
|
|
773
|
+
recipientInstanceDisplayName: assertInstanceDisplayName(value.recipientInstanceDisplayName, "human_confirmation_response_malformed"),
|
|
774
|
+
} : {}),
|
|
775
|
+
clientMessageId: assertIdentifier(value.clientMessageId, "human_confirmation_response_malformed"),
|
|
776
|
+
textDigest: assertHex(value.textDigest, "human_confirmation_response_malformed"),
|
|
777
|
+
createdAtMs: assertNow(value.createdAtMs),
|
|
778
|
+
expiresAtMs: assertNow(value.expiresAtMs),
|
|
779
|
+
proof: assertHex(value.proof, "human_confirmation_response_malformed"),
|
|
780
|
+
};
|
|
781
|
+
return Object.freeze(normalized);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function responseBindingMismatch(action, response) {
|
|
785
|
+
if (response.hostGeneration !== action.hostGeneration) {
|
|
786
|
+
return "human_confirmation_host_generation_mismatch";
|
|
787
|
+
}
|
|
788
|
+
for (const key of [
|
|
789
|
+
"actionId", "principalId", "delegateId", "hostInstallationId", "adapterId",
|
|
790
|
+
"adapterInstanceId", "recipientHelloLynkId", "clientMessageId", "textDigest",
|
|
791
|
+
"createdAtMs", "expiresAtMs",
|
|
792
|
+
...(Object.hasOwn(action, "recipientDelegateId") ? RECIPIENT_INSTANCE_KEYS : []),
|
|
793
|
+
]) {
|
|
794
|
+
if (response[key] !== action[key]) return "human_confirmation_binding_mismatch";
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function createDecisionProof(action, decision) {
|
|
800
|
+
return crypto.createHmac("sha256", action.hmacKey)
|
|
801
|
+
.update(canonicalDecisionMessage({ ...action, decision }), "utf8")
|
|
802
|
+
.digest("hex");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function canonicalDecisionMessage(value) {
|
|
806
|
+
return [
|
|
807
|
+
HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_CONTRACT,
|
|
808
|
+
value.actionId,
|
|
809
|
+
value.decision,
|
|
810
|
+
value.principalId,
|
|
811
|
+
value.delegateId,
|
|
812
|
+
value.hostInstallationId,
|
|
813
|
+
value.adapterId,
|
|
814
|
+
value.adapterInstanceId,
|
|
815
|
+
value.hostGeneration,
|
|
816
|
+
value.recipientHelloLynkId,
|
|
817
|
+
...(Object.hasOwn(value, "recipientDelegateId") ? [value.recipientDelegateId, value.recipientInstanceDisplayName] : []),
|
|
818
|
+
value.clientMessageId,
|
|
819
|
+
value.textDigest,
|
|
820
|
+
String(value.createdAtMs),
|
|
821
|
+
String(value.expiresAtMs),
|
|
822
|
+
value.challenge,
|
|
823
|
+
].join("\n");
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function publicStatus(action, httpStatus, idempotent) {
|
|
827
|
+
return Object.freeze({
|
|
828
|
+
status: httpStatus,
|
|
829
|
+
body: Object.freeze({
|
|
830
|
+
contract: HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_CONTRACT,
|
|
831
|
+
actionId: action.actionId,
|
|
832
|
+
state: action.state,
|
|
833
|
+
code: action.code,
|
|
834
|
+
createdAtMs: action.createdAtMs,
|
|
835
|
+
expiresAtMs: action.expiresAtMs,
|
|
836
|
+
idempotent,
|
|
837
|
+
}),
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function publicTombstone(value) {
|
|
842
|
+
assertPlainObject(value, "human_confirmation_ledger_response_invalid");
|
|
843
|
+
return Object.freeze({
|
|
844
|
+
status: 200,
|
|
845
|
+
body: Object.freeze({
|
|
846
|
+
contract: HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_CONTRACT,
|
|
847
|
+
actionId: assertIdentifier(value.actionId, "human_confirmation_ledger_response_invalid"),
|
|
848
|
+
state: assertTombstoneState(value.state),
|
|
849
|
+
code: value.code == null ? null : safeLedgerCode(value.code),
|
|
850
|
+
createdAtMs: assertNow(value.createdAtMs),
|
|
851
|
+
expiresAtMs: assertNow(value.expiresAtMs),
|
|
852
|
+
idempotent: false,
|
|
853
|
+
}),
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function assertLedger(value) {
|
|
858
|
+
if (!value || value.contract !== HELLOLYNK_NATIVE_HUMAN_CONFIRMATION_LEDGER_CONTRACT
|
|
859
|
+
|| value.durability !== "cross-process"
|
|
860
|
+
|| typeof value.admit !== "function" || typeof value.record !== "function"
|
|
861
|
+
|| typeof value.lookup !== "function") {
|
|
862
|
+
throw codeError("human_confirmation_ledger_invalid", 500);
|
|
863
|
+
}
|
|
864
|
+
return value;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function normalizeLauncherFailure(value) {
|
|
868
|
+
const code = String(value || "");
|
|
869
|
+
const allowed = new Set([
|
|
870
|
+
"human_confirmation_dialog_crashed",
|
|
871
|
+
"human_confirmation_platform_unsupported",
|
|
872
|
+
"human_confirmation_request_too_large",
|
|
873
|
+
"human_confirmation_response_malformed",
|
|
874
|
+
"human_confirmation_stderr_oversize",
|
|
875
|
+
"human_confirmation_stdout_extra",
|
|
876
|
+
"human_confirmation_stdout_oversize",
|
|
877
|
+
"human_confirmation_timed_out",
|
|
878
|
+
]);
|
|
879
|
+
return allowed.has(code) ? code : "human_confirmation_dialog_crashed";
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function failureState(code) {
|
|
883
|
+
return code === "human_confirmation_timed_out" ? "expired" : "failed";
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function scrubActionSecrets(action) {
|
|
887
|
+
action.challenge = null;
|
|
888
|
+
if (Buffer.isBuffer(action.hmacKey)) action.hmacKey.fill(0);
|
|
889
|
+
action.hmacKey = null;
|
|
890
|
+
action.text = null;
|
|
891
|
+
action.nativeAuthority = null;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function assertNativeAuthority(authority) {
|
|
895
|
+
if (!authority) return; // Standalone broker callers do not own a Host attachment.
|
|
896
|
+
try {
|
|
897
|
+
authority.assertCurrent();
|
|
898
|
+
} catch {
|
|
899
|
+
throw codeError("human_confirmation_authority_invalidated", 409);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function hashParts(...parts) {
|
|
904
|
+
return crypto.createHash("sha256").update(parts.join("\0"), "utf8").digest("hex");
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function randomToken(randomBytes, size) {
|
|
908
|
+
const value = Buffer.from(randomBytes(size));
|
|
909
|
+
if (value.length !== size) throw codeError("human_confirmation_random_source_invalid", 500);
|
|
910
|
+
return value.toString("base64url");
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function safeHexEqual(expected, candidate) {
|
|
914
|
+
if (!HEX_64_PATTERN.test(candidate)) return false;
|
|
915
|
+
return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(candidate, "hex"));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function assertText(value) {
|
|
919
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")
|
|
920
|
+
|| Buffer.byteLength(value, "utf8") > MAX_TEXT_BYTES) {
|
|
921
|
+
throw codeError("human_confirmation_text_invalid", 400);
|
|
922
|
+
}
|
|
923
|
+
return value;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function assertIdentifier(value, code) {
|
|
927
|
+
const text = String(value || "");
|
|
928
|
+
if (!IDENTIFIER_PATTERN.test(text)) throw codeError(code, 400);
|
|
929
|
+
return text;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function assertInstanceDisplayName(value, code = "human_confirmation_recipient_instance_display_name_invalid") {
|
|
933
|
+
if (typeof value !== "string" || value.trim() !== value || !value.length
|
|
934
|
+
|| [...value].length > 128 || Buffer.byteLength(value, "utf8") > 512
|
|
935
|
+
|| /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/u.test(value)) {
|
|
936
|
+
throw codeError(code, 400);
|
|
937
|
+
}
|
|
938
|
+
return value;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function recipientInstanceFields(value) {
|
|
942
|
+
return Object.hasOwn(value, "recipientDelegateId") ? {
|
|
943
|
+
recipientDelegateId: value.recipientDelegateId,
|
|
944
|
+
recipientInstanceDisplayName: value.recipientInstanceDisplayName,
|
|
945
|
+
} : {};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function assertHelloLynkId(value, code = "human_confirmation_recipient_hellolynk_id_invalid") {
|
|
949
|
+
const text = String(value || "");
|
|
950
|
+
if (!HELLOLYNK_ID_PATTERN.test(text)) throw codeError(code, 400);
|
|
951
|
+
return text;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function assertHex(value, code) {
|
|
955
|
+
const text = String(value || "").toLowerCase();
|
|
956
|
+
if (!HEX_64_PATTERN.test(text)) throw codeError(code, 500);
|
|
957
|
+
return text;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function assertNow(value) {
|
|
961
|
+
const number = Number(value);
|
|
962
|
+
if (!Number.isSafeInteger(number) || number < 0) {
|
|
963
|
+
throw codeError("human_confirmation_clock_invalid", 500);
|
|
964
|
+
}
|
|
965
|
+
return number;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function assertPlainObject(value, code) {
|
|
969
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
970
|
+
|| (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
|
|
971
|
+
throw codeError(code, 400);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function assertExactKeys(value, expected, code) {
|
|
976
|
+
const actual = Object.keys(value).sort();
|
|
977
|
+
const wanted = [...expected].sort();
|
|
978
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
979
|
+
throw codeError(code, 400);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function assertTombstoneState(value) {
|
|
984
|
+
const state = String(value || "");
|
|
985
|
+
if (!new Set(["approved", "cancelled", "consuming", "expired", "failed", "invalidated"]).has(state)) {
|
|
986
|
+
throw codeError("human_confirmation_ledger_response_invalid", 500);
|
|
987
|
+
}
|
|
988
|
+
return state;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function safeLedgerCode(value) {
|
|
992
|
+
const code = String(value || "");
|
|
993
|
+
if (!/^human_confirmation_[a-z0-9_]{1,100}$/.test(code)) {
|
|
994
|
+
throw codeError("human_confirmation_ledger_response_invalid", 500);
|
|
995
|
+
}
|
|
996
|
+
return code;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function normalizeClock(value) {
|
|
1000
|
+
if (value == null) return Object.freeze({ now: () => Date.now() });
|
|
1001
|
+
if (typeof value === "function") return Object.freeze({ now: value });
|
|
1002
|
+
if (typeof value?.now === "function") return Object.freeze({ now: () => value.now() });
|
|
1003
|
+
throw codeError("human_confirmation_clock_invalid", 500);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function normalizeScheduler(value) {
|
|
1007
|
+
if (value == null) return Object.freeze({
|
|
1008
|
+
setTimeout: (callback, delay) => setTimeout(callback, delay),
|
|
1009
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
1010
|
+
});
|
|
1011
|
+
if (typeof value.setTimeout !== "function" || typeof value.clearTimeout !== "function") {
|
|
1012
|
+
throw codeError("human_confirmation_scheduler_invalid", 500);
|
|
1013
|
+
}
|
|
1014
|
+
return value;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function normalizeDrainTimeout(value) {
|
|
1018
|
+
const accepted = value == null ? 5_000 : Number(value);
|
|
1019
|
+
if (!Number.isSafeInteger(accepted) || accepted < 10 || accepted > 60_000) {
|
|
1020
|
+
throw codeError("human_confirmation_drain_timeout_invalid", 500);
|
|
1021
|
+
}
|
|
1022
|
+
return accepted;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function codeError(code, status = 400, retryAfterMs = undefined) {
|
|
1026
|
+
const error = new Error(code);
|
|
1027
|
+
error.code = code;
|
|
1028
|
+
error.status = status;
|
|
1029
|
+
if (Number.isSafeInteger(retryAfterMs) && retryAfterMs >= 0) error.retryAfterMs = retryAfterMs;
|
|
1030
|
+
return error;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function safeCode(error, fallback) {
|
|
1034
|
+
const code = String(error?.code || "");
|
|
1035
|
+
return /^human_confirmation_[a-z0-9_]{1,100}$/.test(code) ? code : fallback;
|
|
1036
|
+
}
|