@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,457 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const HELLOLYNK_WORKBENCH_ACTION_GRANT_TTL_MS = 60_000;
|
|
4
|
+
export const HELLOLYNK_WORKBENCH_ACTION_GRANT_CAPACITY = 1_024;
|
|
5
|
+
|
|
6
|
+
const MIN_TTL_MS = 30_000;
|
|
7
|
+
const MAX_TTL_MS = 90_000;
|
|
8
|
+
const MAX_CAPACITY = 4_096;
|
|
9
|
+
const MAX_IDENTIFIER_BYTES = 256;
|
|
10
|
+
const MAX_TARGET_BYTES = 1_024;
|
|
11
|
+
const MAX_PAYLOAD_BYTES = 256 * 1_024;
|
|
12
|
+
const MAX_PAYLOAD_DEPTH = 64;
|
|
13
|
+
const TOKEN_BYTES = 32;
|
|
14
|
+
const TOKEN_PREFIX = "hwag_";
|
|
15
|
+
const TOKEN_PATTERN = /^hwag_[A-Za-z0-9_-]{43}$/;
|
|
16
|
+
const ISSUE_KEYS = Object.freeze([
|
|
17
|
+
"adapterId",
|
|
18
|
+
"adapterInstanceId",
|
|
19
|
+
"capabilityId",
|
|
20
|
+
"delegateId",
|
|
21
|
+
"expectedRevision",
|
|
22
|
+
"hostGeneration",
|
|
23
|
+
"idempotencyKey",
|
|
24
|
+
"payload",
|
|
25
|
+
"principalId",
|
|
26
|
+
"target",
|
|
27
|
+
"tool",
|
|
28
|
+
]);
|
|
29
|
+
const CONSUME_KEYS = Object.freeze([...ISSUE_KEYS, "actionGrant"].sort());
|
|
30
|
+
const BINDING_FIELDS = Object.freeze([
|
|
31
|
+
"hostGeneration",
|
|
32
|
+
"principalId",
|
|
33
|
+
"capabilityId",
|
|
34
|
+
"adapterId",
|
|
35
|
+
"delegateId",
|
|
36
|
+
"adapterInstanceId",
|
|
37
|
+
"tool",
|
|
38
|
+
"target",
|
|
39
|
+
"payloadDigest",
|
|
40
|
+
"expectedRevision",
|
|
41
|
+
"idempotencyKey",
|
|
42
|
+
]);
|
|
43
|
+
const AUTHORIZED_RESULT = Object.freeze({ authorized: true });
|
|
44
|
+
const KNOWN_FAILED_OUTCOME = "known-failed";
|
|
45
|
+
const UNKNOWN_OUTCOME = "unknown";
|
|
46
|
+
const REGISTRY_INTERNALS = new WeakMap();
|
|
47
|
+
|
|
48
|
+
export class HelloLynkWorkbenchActionGrantError extends Error {
|
|
49
|
+
constructor(code, statusCode, outcome = null) {
|
|
50
|
+
super(code);
|
|
51
|
+
this.name = "HelloLynkWorkbenchActionGrantError";
|
|
52
|
+
this.code = code;
|
|
53
|
+
this.statusCode = statusCode;
|
|
54
|
+
if (outcome != null) this.outcome = outcome;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* This registry is deliberately process-local. Recreating the Host creates an
|
|
60
|
+
* empty registry, so a grant can never survive a Host restart. The raw token and
|
|
61
|
+
* original payload are never retained: the Map key is SHA-256(token), while the
|
|
62
|
+
* record contains only normalized authority bindings and a payload digest.
|
|
63
|
+
*/
|
|
64
|
+
export function createHelloLynkWorkbenchActionGrantRegistry(options = {}) {
|
|
65
|
+
const clock = normalizeClock(options.clock);
|
|
66
|
+
const randomBytes = normalizeRandomBytes(options.randomBytes);
|
|
67
|
+
const ttlMs = normalizeTtl(options.ttlMs);
|
|
68
|
+
const capacity = normalizeCapacity(options.capacity);
|
|
69
|
+
const grants = new Map();
|
|
70
|
+
const tombstones = new Map();
|
|
71
|
+
let lastObservedNowMs = null;
|
|
72
|
+
|
|
73
|
+
function issue(value) {
|
|
74
|
+
const binding = normalizeBinding(value, ISSUE_KEYS, "issue");
|
|
75
|
+
const nowMs = monotonicNow();
|
|
76
|
+
pruneExpired(nowMs);
|
|
77
|
+
if (grants.size >= capacity) {
|
|
78
|
+
throw failure("host_workbench_action_grant_capacity_exceeded", 503);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
82
|
+
const tokenBytes = readRandomBytes(randomBytes);
|
|
83
|
+
const actionGrant = `${TOKEN_PREFIX}${tokenBytes.toString("base64url")}`;
|
|
84
|
+
if (!TOKEN_PATTERN.test(actionGrant)) continue;
|
|
85
|
+
const tokenDigest = digestText(actionGrant);
|
|
86
|
+
if (grants.has(tokenDigest) || tombstones.has(tokenDigest)) continue;
|
|
87
|
+
|
|
88
|
+
const expiresAtMs = nowMs + ttlMs;
|
|
89
|
+
if (!Number.isSafeInteger(expiresAtMs)) {
|
|
90
|
+
throw failure("host_workbench_action_grant_clock_invalid", 500);
|
|
91
|
+
}
|
|
92
|
+
grants.set(tokenDigest, Object.freeze({ ...binding, expiresAtMs }));
|
|
93
|
+
return Object.freeze({ actionGrant, expiresAtMs });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw failure("host_workbench_action_grant_generation_failed", 500);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function consume(value) {
|
|
100
|
+
// Extract and burn a well-formed, known token before validating any other
|
|
101
|
+
// field. A malformed binding therefore cannot be retried with the same grant.
|
|
102
|
+
// A successful burn becomes an outcome-unknown tombstone synchronously,
|
|
103
|
+
// before the caller can enter an attachment check or an async handler.
|
|
104
|
+
const rawToken = ownDataProperty(value, "actionGrant");
|
|
105
|
+
const tokenDigest = typeof rawToken === "string" && TOKEN_PATTERN.test(rawToken)
|
|
106
|
+
? digestText(rawToken)
|
|
107
|
+
: null;
|
|
108
|
+
if (!tokenDigest) throw invalidGrant(KNOWN_FAILED_OUTCOME);
|
|
109
|
+
|
|
110
|
+
const record = grants.get(tokenDigest);
|
|
111
|
+
if (!record) {
|
|
112
|
+
const tombstone = tombstones.get(tokenDigest);
|
|
113
|
+
// A syntactically valid but unrecognized token may be a consumed token
|
|
114
|
+
// whose bounded tombstone has expired or been evicted. We cannot prove
|
|
115
|
+
// that its write never started, so the safe public outcome is unknown.
|
|
116
|
+
throw invalidGrant(tombstone?.outcome ?? UNKNOWN_OUTCOME);
|
|
117
|
+
}
|
|
118
|
+
grants.delete(tokenDigest);
|
|
119
|
+
|
|
120
|
+
let binding;
|
|
121
|
+
try {
|
|
122
|
+
binding = normalizeBinding(value, CONSUME_KEYS, "consume");
|
|
123
|
+
} catch {
|
|
124
|
+
rememberTombstone(tokenDigest, KNOWN_FAILED_OUTCOME, record.expiresAtMs);
|
|
125
|
+
throw invalidGrant(KNOWN_FAILED_OUTCOME);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let nowMs;
|
|
129
|
+
try {
|
|
130
|
+
nowMs = monotonicNow();
|
|
131
|
+
} catch {
|
|
132
|
+
// This record was still active and has not crossed the Host execution
|
|
133
|
+
// boundary, so a clock failure can be classified as known-failed.
|
|
134
|
+
rememberTombstone(tokenDigest, KNOWN_FAILED_OUTCOME, record.expiresAtMs);
|
|
135
|
+
throw invalidGrant(KNOWN_FAILED_OUTCOME);
|
|
136
|
+
}
|
|
137
|
+
if (record.expiresAtMs <= nowMs) {
|
|
138
|
+
rememberTombstone(tokenDigest, KNOWN_FAILED_OUTCOME, nowMs + ttlMs);
|
|
139
|
+
throw invalidGrant(KNOWN_FAILED_OUTCOME);
|
|
140
|
+
}
|
|
141
|
+
for (const field of BINDING_FIELDS) {
|
|
142
|
+
if (record[field] !== binding[field]) {
|
|
143
|
+
rememberTombstone(tokenDigest, KNOWN_FAILED_OUTCOME, nowMs + ttlMs);
|
|
144
|
+
throw invalidGrant(KNOWN_FAILED_OUTCOME);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
rememberTombstone(tokenDigest, UNKNOWN_OUTCOME, nowMs + ttlMs);
|
|
148
|
+
return AUTHORIZED_RESULT;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function prune() {
|
|
152
|
+
return pruneExpired(monotonicNow());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function monotonicNow() {
|
|
156
|
+
let nowMs;
|
|
157
|
+
try {
|
|
158
|
+
nowMs = readNow(clock);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
failClosedActiveGrants();
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
if (lastObservedNowMs != null && nowMs < lastObservedNowMs) {
|
|
164
|
+
// Keep the high-water mark: the registry remains fail-closed until the
|
|
165
|
+
// clock catches up, instead of silently extending outstanding TTLs.
|
|
166
|
+
failClosedActiveGrants();
|
|
167
|
+
throw failure("host_workbench_action_grant_clock_invalid", 500);
|
|
168
|
+
}
|
|
169
|
+
lastObservedNowMs = nowMs;
|
|
170
|
+
return nowMs;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function pruneExpired(nowMs) {
|
|
174
|
+
let pruned = 0;
|
|
175
|
+
for (const [tokenDigest, grant] of grants) {
|
|
176
|
+
if (grant.expiresAtMs <= nowMs) {
|
|
177
|
+
grants.delete(tokenDigest);
|
|
178
|
+
rememberTombstone(tokenDigest, KNOWN_FAILED_OUTCOME, nowMs + ttlMs);
|
|
179
|
+
pruned += 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const [tokenDigest, tombstone] of tombstones) {
|
|
183
|
+
if (tombstone.expiresAtMs <= nowMs) {
|
|
184
|
+
tombstones.delete(tokenDigest);
|
|
185
|
+
pruned += 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return pruned;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function failClosedActiveGrants() {
|
|
192
|
+
const fallbackExpiry = (lastObservedNowMs ?? 0) + ttlMs;
|
|
193
|
+
for (const [tokenDigest, grant] of grants) {
|
|
194
|
+
rememberTombstone(
|
|
195
|
+
tokenDigest,
|
|
196
|
+
KNOWN_FAILED_OUTCOME,
|
|
197
|
+
Math.max(grant.expiresAtMs, fallbackExpiry),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
grants.clear();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function rememberTombstone(tokenDigest, outcome, expiresAtMs) {
|
|
204
|
+
if (tombstones.has(tokenDigest)) tombstones.delete(tokenDigest);
|
|
205
|
+
while (tombstones.size >= capacity) {
|
|
206
|
+
tombstones.delete(tombstones.keys().next().value);
|
|
207
|
+
}
|
|
208
|
+
tombstones.set(tokenDigest, Object.freeze({ outcome, expiresAtMs }));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const registry = Object.freeze({
|
|
212
|
+
issue,
|
|
213
|
+
consume,
|
|
214
|
+
prune,
|
|
215
|
+
get size() {
|
|
216
|
+
pruneExpired(monotonicNow());
|
|
217
|
+
return grants.size;
|
|
218
|
+
},
|
|
219
|
+
get capacity() {
|
|
220
|
+
return capacity;
|
|
221
|
+
},
|
|
222
|
+
get ttlMs() {
|
|
223
|
+
return ttlMs;
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
REGISTRY_INTERNALS.set(registry, Object.freeze({ grants, rememberTombstone, tombstones }));
|
|
227
|
+
return registry;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function rejectConsumedHelloLynkWorkbenchActionGrant(registry, actionGrant) {
|
|
231
|
+
const internal = REGISTRY_INTERNALS.get(registry);
|
|
232
|
+
if (!internal) throw failure("host_workbench_action_grant_registry_invalid", 500);
|
|
233
|
+
if (typeof actionGrant !== "string" || !TOKEN_PATTERN.test(actionGrant)) return false;
|
|
234
|
+
const tokenDigest = digestText(actionGrant);
|
|
235
|
+
const tombstone = internal.tombstones.get(tokenDigest);
|
|
236
|
+
if (!tombstone || tombstone.outcome !== UNKNOWN_OUTCOME) return false;
|
|
237
|
+
internal.rememberTombstone(tokenDigest, KNOWN_FAILED_OUTCOME, tombstone.expiresAtMs);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function revokeHelloLynkWorkbenchActionGrants(registry, value) {
|
|
242
|
+
const internal = REGISTRY_INTERNALS.get(registry);
|
|
243
|
+
if (!internal) throw failure("host_workbench_action_grant_registry_invalid", 500);
|
|
244
|
+
const hasAttachment = value && Object.hasOwn(value, "adapterInstanceId");
|
|
245
|
+
try {
|
|
246
|
+
assertExactObject(
|
|
247
|
+
value,
|
|
248
|
+
hasAttachment
|
|
249
|
+
? ["adapterInstanceId", "capabilityId", "hostGeneration"]
|
|
250
|
+
: ["capabilityId", "hostGeneration"],
|
|
251
|
+
);
|
|
252
|
+
const authority = {
|
|
253
|
+
hostGeneration: boundedString(value.hostGeneration, MAX_IDENTIFIER_BYTES),
|
|
254
|
+
capabilityId: boundedString(value.capabilityId, MAX_IDENTIFIER_BYTES),
|
|
255
|
+
...(hasAttachment ? {
|
|
256
|
+
adapterInstanceId: boundedString(value.adapterInstanceId, MAX_IDENTIFIER_BYTES),
|
|
257
|
+
} : {}),
|
|
258
|
+
};
|
|
259
|
+
let revoked = 0;
|
|
260
|
+
for (const [tokenDigest, grant] of internal.grants) {
|
|
261
|
+
if (grant.hostGeneration === authority.hostGeneration
|
|
262
|
+
&& grant.capabilityId === authority.capabilityId
|
|
263
|
+
&& (!hasAttachment || grant.adapterInstanceId === authority.adapterInstanceId)) {
|
|
264
|
+
internal.grants.delete(tokenDigest);
|
|
265
|
+
internal.rememberTombstone(
|
|
266
|
+
tokenDigest,
|
|
267
|
+
KNOWN_FAILED_OUTCOME,
|
|
268
|
+
grant.expiresAtMs,
|
|
269
|
+
);
|
|
270
|
+
revoked += 1;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return revoked;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error instanceof HelloLynkWorkbenchActionGrantError) throw error;
|
|
276
|
+
throw failure("host_workbench_action_grant_binding_invalid", 400);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizeBinding(value, expectedKeys, operation) {
|
|
281
|
+
try {
|
|
282
|
+
assertExactObject(value, expectedKeys);
|
|
283
|
+
return Object.freeze({
|
|
284
|
+
hostGeneration: boundedString(value.hostGeneration, MAX_IDENTIFIER_BYTES),
|
|
285
|
+
principalId: boundedString(value.principalId, MAX_IDENTIFIER_BYTES),
|
|
286
|
+
capabilityId: boundedString(value.capabilityId, MAX_IDENTIFIER_BYTES),
|
|
287
|
+
adapterId: boundedString(value.adapterId, MAX_IDENTIFIER_BYTES),
|
|
288
|
+
delegateId: boundedString(value.delegateId, MAX_IDENTIFIER_BYTES),
|
|
289
|
+
adapterInstanceId: boundedString(value.adapterInstanceId, MAX_IDENTIFIER_BYTES),
|
|
290
|
+
tool: boundedString(value.tool, MAX_IDENTIFIER_BYTES),
|
|
291
|
+
target: boundedString(value.target, MAX_TARGET_BYTES),
|
|
292
|
+
payloadDigest: digestPayload(value.payload),
|
|
293
|
+
expectedRevision: value.expectedRevision == null
|
|
294
|
+
? null
|
|
295
|
+
: boundedString(value.expectedRevision, MAX_IDENTIFIER_BYTES),
|
|
296
|
+
idempotencyKey: boundedString(value.idempotencyKey, MAX_IDENTIFIER_BYTES),
|
|
297
|
+
});
|
|
298
|
+
} catch {
|
|
299
|
+
if (operation === "consume") throw invalidGrant();
|
|
300
|
+
throw failure("host_workbench_action_grant_binding_invalid", 400);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function assertExactObject(value, expectedKeys) {
|
|
305
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("invalid");
|
|
306
|
+
const prototype = Object.getPrototypeOf(value);
|
|
307
|
+
if (prototype !== Object.prototype && prototype !== null) throw new TypeError("invalid");
|
|
308
|
+
if (Object.getOwnPropertySymbols(value).length !== 0) throw new TypeError("invalid");
|
|
309
|
+
const actual = Object.keys(value).sort();
|
|
310
|
+
if (actual.length !== expectedKeys.length) throw new TypeError("invalid");
|
|
311
|
+
for (let index = 0; index < expectedKeys.length; index += 1) {
|
|
312
|
+
if (actual[index] !== expectedKeys[index]) throw new TypeError("invalid");
|
|
313
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, actual[index]);
|
|
314
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value")) throw new TypeError("invalid");
|
|
315
|
+
}
|
|
316
|
+
if (Object.getOwnPropertyNames(value).length !== actual.length) throw new TypeError("invalid");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function boundedString(value, maxBytes) {
|
|
320
|
+
if (typeof value !== "string" || value.length === 0 || value !== value.trim()) {
|
|
321
|
+
throw new TypeError("invalid");
|
|
322
|
+
}
|
|
323
|
+
if (Buffer.byteLength(value, "utf8") > maxBytes) throw new TypeError("invalid");
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function digestPayload(value) {
|
|
328
|
+
const canonical = canonicalJson(value, new Set(), 0);
|
|
329
|
+
if (Buffer.byteLength(canonical, "utf8") > MAX_PAYLOAD_BYTES) throw new TypeError("invalid");
|
|
330
|
+
return digestText(canonical);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function canonicalJson(value, ancestors, depth) {
|
|
334
|
+
if (depth > MAX_PAYLOAD_DEPTH) throw new TypeError("invalid");
|
|
335
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
336
|
+
return JSON.stringify(value);
|
|
337
|
+
}
|
|
338
|
+
if (typeof value === "number") {
|
|
339
|
+
if (!Number.isFinite(value)) throw new TypeError("invalid");
|
|
340
|
+
return JSON.stringify(value);
|
|
341
|
+
}
|
|
342
|
+
if (!value || typeof value !== "object") throw new TypeError("invalid");
|
|
343
|
+
if (ancestors.has(value)) throw new TypeError("invalid");
|
|
344
|
+
if (Object.getOwnPropertySymbols(value).length !== 0) throw new TypeError("invalid");
|
|
345
|
+
|
|
346
|
+
ancestors.add(value);
|
|
347
|
+
try {
|
|
348
|
+
if (Array.isArray(value)) {
|
|
349
|
+
if (Object.keys(value).length !== value.length) throw new TypeError("invalid");
|
|
350
|
+
if (Object.getOwnPropertyNames(value).length !== value.length + 1) {
|
|
351
|
+
throw new TypeError("invalid");
|
|
352
|
+
}
|
|
353
|
+
const entries = [];
|
|
354
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
355
|
+
if (!Object.hasOwn(value, index)) throw new TypeError("invalid");
|
|
356
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
357
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
358
|
+
throw new TypeError("invalid");
|
|
359
|
+
}
|
|
360
|
+
entries.push(canonicalJson(value[index], ancestors, depth + 1));
|
|
361
|
+
}
|
|
362
|
+
return `[${entries.join(",")}]`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const prototype = Object.getPrototypeOf(value);
|
|
366
|
+
if (prototype !== Object.prototype && prototype !== null) throw new TypeError("invalid");
|
|
367
|
+
const keys = Object.keys(value).sort();
|
|
368
|
+
if (Object.getOwnPropertyNames(value).length !== keys.length) throw new TypeError("invalid");
|
|
369
|
+
for (const key of keys) {
|
|
370
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
371
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
372
|
+
throw new TypeError("invalid");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const entries = keys.map((key) => (
|
|
376
|
+
`${JSON.stringify(key)}:${canonicalJson(value[key], ancestors, depth + 1)}`
|
|
377
|
+
));
|
|
378
|
+
return `{${entries.join(",")}}`;
|
|
379
|
+
} finally {
|
|
380
|
+
ancestors.delete(value);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function ownDataProperty(value, key) {
|
|
385
|
+
if (!value || typeof value !== "object") return null;
|
|
386
|
+
try {
|
|
387
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
388
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : null;
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function normalizeClock(value) {
|
|
395
|
+
if (value == null) return Object.freeze({ now: () => Date.now() });
|
|
396
|
+
if (!value || typeof value.now !== "function") {
|
|
397
|
+
throw failure("host_workbench_action_grant_clock_invalid", 500);
|
|
398
|
+
}
|
|
399
|
+
return value;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function normalizeRandomBytes(value) {
|
|
403
|
+
if (value == null) return crypto.randomBytes;
|
|
404
|
+
if (typeof value !== "function") {
|
|
405
|
+
throw failure("host_workbench_action_grant_random_source_invalid", 500);
|
|
406
|
+
}
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function normalizeTtl(value) {
|
|
411
|
+
const ttlMs = value ?? HELLOLYNK_WORKBENCH_ACTION_GRANT_TTL_MS;
|
|
412
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs < MIN_TTL_MS || ttlMs > MAX_TTL_MS) {
|
|
413
|
+
throw failure("host_workbench_action_grant_ttl_invalid", 500);
|
|
414
|
+
}
|
|
415
|
+
return ttlMs;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function normalizeCapacity(value) {
|
|
419
|
+
const capacity = value ?? HELLOLYNK_WORKBENCH_ACTION_GRANT_CAPACITY;
|
|
420
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1 || capacity > MAX_CAPACITY) {
|
|
421
|
+
throw failure("host_workbench_action_grant_capacity_invalid", 500);
|
|
422
|
+
}
|
|
423
|
+
return capacity;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function readNow(clock) {
|
|
427
|
+
const nowMs = clock.now();
|
|
428
|
+
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
|
|
429
|
+
throw failure("host_workbench_action_grant_clock_invalid", 500);
|
|
430
|
+
}
|
|
431
|
+
return nowMs;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function readRandomBytes(randomBytes) {
|
|
435
|
+
let value;
|
|
436
|
+
try {
|
|
437
|
+
value = randomBytes(TOKEN_BYTES);
|
|
438
|
+
} catch {
|
|
439
|
+
throw failure("host_workbench_action_grant_generation_failed", 500);
|
|
440
|
+
}
|
|
441
|
+
if (!(value instanceof Uint8Array) || value.byteLength !== TOKEN_BYTES) {
|
|
442
|
+
throw failure("host_workbench_action_grant_generation_failed", 500);
|
|
443
|
+
}
|
|
444
|
+
return Buffer.from(value);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function digestText(value) {
|
|
448
|
+
return crypto.createHash("sha256").update(value, "utf8").digest("hex");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function invalidGrant(outcome) {
|
|
452
|
+
return failure("host_workbench_action_grant_invalid", 403, outcome);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function failure(code, statusCode, outcome = null) {
|
|
456
|
+
return new HelloLynkWorkbenchActionGrantError(code, statusCode, outcome);
|
|
457
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import safety from "./hellolynk-runtime-safety.cjs";
|
|
7
|
+
import { createNeutralHostInstallationStore, defaultNeutralHostStoreRoot } from "./hellolynk-neutral-host-installation-store.mjs";
|
|
8
|
+
import { verifyPinnedNeutralHostRuntime } from "./hellolynk-neutral-host-launcher.mjs";
|
|
9
|
+
import { prepareWindowsClientRoot, secureWindowsClientReleaseTree } from "./hellolynk-platform-credential-store.mjs";
|
|
10
|
+
import { runRelayClientCli } from "./hellolynk-relay-client-cli.mjs";
|
|
11
|
+
import { createHelloLynkAccountLauncher } from "./hellolynk-neutral-host-assembly.mjs";
|
|
12
|
+
import { ACCOUNT_CLIENTS, clientAccountBaseRoot, createClientAccountActions } from "./hellolynk-client-account.mjs";
|
|
13
|
+
|
|
14
|
+
const CLIENTS = new Set(ACCOUNT_CLIENTS);
|
|
15
|
+
const CONFIRMATION_URL = /^https:\/\/hellolynk\.com\/account\?deviceRequest=devreq_[a-f0-9]{48}$/;
|
|
16
|
+
|
|
17
|
+
// Restricted to our account pages, including the current device-confirmation
|
|
18
|
+
// link. No arbitrary URL or shell text can come from the browser.
|
|
19
|
+
export function createWorkbenchAccountOpener(options = {}) {
|
|
20
|
+
const launch = createHelloLynkAccountLauncher({ ...options, baseUrl: "https://hellolynk.com" });
|
|
21
|
+
return async ({ url } = {}) => {
|
|
22
|
+
if (typeof url !== "string" || (!/^https:\/\/hellolynk\.com\/account(?:#(?:keys|devices))?$/.test(url)
|
|
23
|
+
&& !CONFIRMATION_URL.test(url))) throw code("workbench_portal_target_invalid", 400);
|
|
24
|
+
const parsed = new URL(url);
|
|
25
|
+
const deviceRequest = parsed.searchParams.get("deviceRequest");
|
|
26
|
+
const result = await launch({ target: parsed.hash.slice(1) || "account", ...(deviceRequest ? { deviceRequest } : {}) });
|
|
27
|
+
if (!result.opened) throw code("workbench_portal_open_failed", 502);
|
|
28
|
+
return { opened: true };
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isWorkbenchConnectionRequired(error) {
|
|
33
|
+
return ["host_installation_missing", "host_delegate_binding_missing", "workbench_connection_required"].includes(error?.code)
|
|
34
|
+
|| /^neutral_(?:codex|openclaw|trae_work|workbuddy|cursor|dsh|claude_code)_binding_configuration_required$/.test(String(error?.code || ""));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function workbenchConnectionError(error) {
|
|
38
|
+
return isWorkbenchConnectionRequired(error) ? code("workbench_connection_required", 409) : error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateWorkbenchConnectionInput(value) {
|
|
42
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
43
|
+
|| Object.keys(value).sort().join(",") !== "hellolynkId,instanceName"
|
|
44
|
+
|| typeof value.hellolynkId !== "string"
|
|
45
|
+
|| !safety.classifyHelloLynkStableId(value.hellolynkId)) throw code("workbench_connect_id_invalid", 400);
|
|
46
|
+
if (typeof value.instanceName !== "string" || [...value.instanceName].length > 80
|
|
47
|
+
|| /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/u.test(value.instanceName)) {
|
|
48
|
+
throw code("workbench_connect_name_invalid", 400);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
hellolynkId: value.hellolynkId,
|
|
52
|
+
// The enrollment layer has the durable intent needed to distinguish an
|
|
53
|
+
// exact legacy retry from a new name. Preserve the value until then.
|
|
54
|
+
instanceName: value.instanceName,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// One small adapter for the existing enrollment and binding commands. Client,
|
|
59
|
+
// package and local store come from the installed integration, never the page.
|
|
60
|
+
export function createWorkbenchConnection(options) {
|
|
61
|
+
if (!CLIENTS.has(options?.client) || !path.isAbsolute(options?.releaseRoot || "")) throw code("workbench_connect_configuration_invalid");
|
|
62
|
+
const env = options.env || process.env;
|
|
63
|
+
const storeRoot = options.storeRoot || env.HELLOLYNK_NEUTRAL_STORE_ROOT || defaultNeutralHostStoreRoot("win32", env);
|
|
64
|
+
if (!path.isAbsolute(storeRoot)) throw code("workbench_connect_configuration_invalid");
|
|
65
|
+
let busy = false;
|
|
66
|
+
const accounts = createClientAccountActions(options);
|
|
67
|
+
const connect = async (input, revision = accounts.readAccountState().revision) => {
|
|
68
|
+
const validated = validateWorkbenchConnectionInput(input);
|
|
69
|
+
if (busy) throw code("workbench_connect_busy", 409);
|
|
70
|
+
busy = true;
|
|
71
|
+
try {
|
|
72
|
+
const result = await accounts.runForAccount(revision, (selectedRoot) => executeJson(
|
|
73
|
+
path.join(options.releaseRoot, "runtime/core/hellolynk-workbench-connection.mjs"),
|
|
74
|
+
["connect", options.client, options.releaseRoot, selectedRoot], env, validated));
|
|
75
|
+
if (result?.status === "confirmation_required" && CONFIRMATION_URL.test(result.verificationUrl)) {
|
|
76
|
+
return { status: result.status, verificationUrl: result.verificationUrl };
|
|
77
|
+
}
|
|
78
|
+
if (result?.status !== "registered") throw code("workbench_connect_result_invalid", 502);
|
|
79
|
+
return publicRegistration(result);
|
|
80
|
+
} finally { busy = false; }
|
|
81
|
+
};
|
|
82
|
+
return Object.assign(connect, accounts);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createWorkbenchAccountActions(options) {
|
|
86
|
+
return createClientAccountActions(options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function connectAccount(client, sourceRoot, storeRoot, input) {
|
|
90
|
+
if (process.platform !== "win32") throw code("workbench_connect_windows_only", 400);
|
|
91
|
+
if (!CLIENTS.has(client) || !path.isAbsolute(sourceRoot) || !path.isAbsolute(storeRoot)
|
|
92
|
+
|| path.basename(storeRoot).toLowerCase() !== "host") throw code("workbench_connect_configuration_invalid");
|
|
93
|
+
const value = validateWorkbenchConnectionInput(input);
|
|
94
|
+
const currentRoot = path.dirname(storeRoot);
|
|
95
|
+
const releaseCurrentRoot = path.dirname(clientAccountBaseRoot(storeRoot));
|
|
96
|
+
const profileId = `hellolynk-host-${client}-v2`;
|
|
97
|
+
const profileManifest = path.join(storeRoot, "profile-manifest-v1.json");
|
|
98
|
+
const pin = JSON.parse(await fs.readFile(path.join(sourceRoot, "runtime/neutral-host-release-pin.json"), "utf8"));
|
|
99
|
+
const binding = {
|
|
100
|
+
adapterId: client, profileId, version: 2, scopes: [], region: "intl",
|
|
101
|
+
principalId: "connection-preflight", delegateId: "connection-preflight", hostInstallationId: "connection-preflight",
|
|
102
|
+
releaseRoot: sourceRoot, releaseManifestSha256: pin.manifestSha256,
|
|
103
|
+
runnerRelativePath: "runtime/neutral-host-bootstrap.mjs", runtimeRoot: path.join(storeRoot, "runtime"), profileManifestPath: profileManifest,
|
|
104
|
+
};
|
|
105
|
+
await verifyPinnedNeutralHostRuntime(binding);
|
|
106
|
+
await prepareWindowsClientRoot(currentRoot);
|
|
107
|
+
const releaseRoot = await retainRelease(binding, releaseCurrentRoot, client);
|
|
108
|
+
await createNeutralHostInstallationStore({ root: storeRoot }).createId();
|
|
109
|
+
const activationArgs = ["--client", client, "--final-current-root", currentRoot, "--root", storeRoot,
|
|
110
|
+
"--release-current-root", releaseCurrentRoot,
|
|
111
|
+
"--release-root", releaseRoot, "--expected-release-manifest-sha256", pin.manifestSha256, "--profile-manifest", profileManifest];
|
|
112
|
+
const activationScript = path.join(releaseRoot, "runtime/connect-activate.mjs");
|
|
113
|
+
const preflight = await executeJson(activationScript, [...activationArgs, "--preflight", "true", "--pre-bind", "true"], process.env);
|
|
114
|
+
if (preflight?.ok !== true || preflight.preflight !== true || preflight.preBind !== true || preflight.readOnly !== true
|
|
115
|
+
|| preflight.releaseManifestSha256 !== pin.manifestSha256 || preflight.clients?.length !== 1
|
|
116
|
+
|| preflight.clients[0]?.adapterId !== client || preflight.clients[0]?.bindingUpdated !== false) throw code("workbench_connect_preflight_invalid");
|
|
117
|
+
const result = await runRelayClientCli(["connect", "--hellolynk-id", value.hellolynkId,
|
|
118
|
+
"--base-url", "https://hellolynk.com", "--root", currentRoot, "--neutral-host-root", storeRoot,
|
|
119
|
+
"--adapter", client, "--profile", profileId, ...(value.instanceName ? ["--instance-name", value.instanceName] : [])]);
|
|
120
|
+
if (result?.ok !== true || result.hellolynkId !== value.hellolynkId) throw code("workbench_connect_result_invalid");
|
|
121
|
+
if (result.status === "confirmation_required" && CONFIRMATION_URL.test(result.verificationUrl)) {
|
|
122
|
+
return { status: result.status, verificationUrl: result.verificationUrl };
|
|
123
|
+
}
|
|
124
|
+
if (result.status !== "registered" || result.clientType !== client || result.profileId !== profileId
|
|
125
|
+
|| result.hostProfileRegistered !== true || result.configPath !== path.join(currentRoot, "profiles", `${profileId}.json`)) {
|
|
126
|
+
throw code("workbench_connect_result_invalid");
|
|
127
|
+
}
|
|
128
|
+
const activated = await executeJson(activationScript, activationArgs, process.env);
|
|
129
|
+
if (activated?.ok !== true || activated.releaseManifestSha256 !== pin.manifestSha256
|
|
130
|
+
|| activated.clients?.length !== 1 || activated.clients[0]?.adapterId !== client) throw code("workbench_connect_activation_invalid");
|
|
131
|
+
return publicRegistration(result);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function publicRegistration(result) {
|
|
135
|
+
return { status: "registered",
|
|
136
|
+
...(typeof result.instanceName === "string" ? { instanceName: result.instanceName } : {}),
|
|
137
|
+
...(result.recoveredExistingName === true ? { recoveredExistingName: true } : {}) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function retainRelease(binding, currentRoot, client) {
|
|
141
|
+
const releases = path.join(currentRoot, "releases");
|
|
142
|
+
const sourceRoot = path.resolve(binding.releaseRoot);
|
|
143
|
+
// Normal installers already put the package here. Codex's disposable plugin
|
|
144
|
+
// cache needs a durable copy of the verified runtime before creating a binding.
|
|
145
|
+
if (sourceRoot.startsWith(`${releases}${path.sep}`)) {
|
|
146
|
+
await secureWindowsClientReleaseTree(currentRoot, path.relative(releases, sourceRoot).split(path.sep)[0], { verifyOnly: true });
|
|
147
|
+
return sourceRoot;
|
|
148
|
+
}
|
|
149
|
+
// Prefer the installed package that supplied this exact Codex cache. This
|
|
150
|
+
// keeps the binding and installer diagnostics pointed at the same release.
|
|
151
|
+
if (client === "codex") {
|
|
152
|
+
for (const entry of (await fs.readdir(releases, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
153
|
+
if (!entry.isDirectory() || !/^codex-[a-f0-9]{64}$/.test(entry.name)) continue;
|
|
154
|
+
const candidate = path.join(releases, entry.name, "plugins/hellolynk-codex");
|
|
155
|
+
let pin;
|
|
156
|
+
try { pin = JSON.parse(await fs.readFile(path.join(candidate, "runtime/neutral-host-release-pin.json"), "utf8")); }
|
|
157
|
+
catch { continue; }
|
|
158
|
+
if (pin.manifestSha256 !== binding.releaseManifestSha256) continue;
|
|
159
|
+
await secureWindowsClientReleaseTree(currentRoot, entry.name, { verifyOnly: true });
|
|
160
|
+
await verifyPinnedNeutralHostRuntime({ ...binding, releaseRoot: candidate });
|
|
161
|
+
return candidate;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const id = `${client}-connection-${binding.releaseManifestSha256}`;
|
|
165
|
+
const target = path.join(releases, id);
|
|
166
|
+
try {
|
|
167
|
+
await fs.lstat(target);
|
|
168
|
+
await secureWindowsClientReleaseTree(currentRoot, id, { verifyOnly: true });
|
|
169
|
+
await verifyPinnedNeutralHostRuntime({ ...binding, releaseRoot: target });
|
|
170
|
+
return target;
|
|
171
|
+
} catch (error) { if (error?.code !== "ENOENT") throw error; }
|
|
172
|
+
const stageId = `.${id}.install-${crypto.randomUUID()}`;
|
|
173
|
+
const stage = path.join(releases, stageId);
|
|
174
|
+
await fs.mkdir(stage, { recursive: true, mode: 0o700 });
|
|
175
|
+
try {
|
|
176
|
+
const manifestPath = path.join(sourceRoot, "runtime/neutral-host-release-manifest.json");
|
|
177
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
178
|
+
const paths = ["runtime/neutral-host-release-pin.json", "runtime/neutral-host-release-manifest.json",
|
|
179
|
+
...manifest.files.map((item) => item.path.startsWith("release/") ? item.path.slice(8) : `runtime/${item.path}`)];
|
|
180
|
+
for (const relative of paths) {
|
|
181
|
+
const destination = path.resolve(stage, relative);
|
|
182
|
+
if (!destination.startsWith(`${stage}${path.sep}`)) throw code("workbench_connect_release_invalid");
|
|
183
|
+
await fs.mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
184
|
+
await fs.writeFile(destination, await fs.readFile(path.join(sourceRoot, relative)), { flag: "wx", mode: 0o600 });
|
|
185
|
+
}
|
|
186
|
+
await verifyPinnedNeutralHostRuntime({ ...binding, releaseRoot: stage });
|
|
187
|
+
await secureWindowsClientReleaseTree(currentRoot, stageId);
|
|
188
|
+
try { await fs.rename(stage, target); }
|
|
189
|
+
catch (error) { if (!["EEXIST", "ENOTEMPTY", "EPERM"].includes(error?.code)) throw error; }
|
|
190
|
+
await secureWindowsClientReleaseTree(currentRoot, id, { verifyOnly: true });
|
|
191
|
+
await verifyPinnedNeutralHostRuntime({ ...binding, releaseRoot: target });
|
|
192
|
+
return target;
|
|
193
|
+
} finally {
|
|
194
|
+
if (path.dirname(stage) === releases && path.basename(stage) === stageId) await fs.rm(stage, { recursive: true, force: true }).catch(() => {});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function executeJson(script, args, env, input) {
|
|
199
|
+
const executable = await safety.resolveHelloLynkStandaloneNode({ env });
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
const child = execFile(executable, [script, ...args], {
|
|
202
|
+
cwd: path.dirname(script), shell: false, windowsHide: true, timeout: 150_000,
|
|
203
|
+
maxBuffer: 256 * 1024, encoding: "utf8", env: safety.standaloneHelloLynkNodeEnvironment(env),
|
|
204
|
+
}, (error, stdout, stderr) => {
|
|
205
|
+
if (error) {
|
|
206
|
+
let failure; try { failure = JSON.parse(stderr); } catch {}
|
|
207
|
+
const safe = failure?.error;
|
|
208
|
+
return reject(code(typeof safe === "string" && /^[a-z][a-z0-9_]{1,127}$/.test(safe)
|
|
209
|
+
? safe : error.killed ? "workbench_connect_timeout" : "workbench_connect_failed"));
|
|
210
|
+
}
|
|
211
|
+
try { resolve(JSON.parse(stdout)); } catch { reject(code("workbench_connect_result_invalid")); }
|
|
212
|
+
});
|
|
213
|
+
child.stdin?.on("error", () => {}); // execFile's callback reports early exit.
|
|
214
|
+
child.stdin?.end(input ? JSON.stringify(input) : undefined);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function code(value, status = 503) { return Object.assign(new Error(value), { code: value, status }); }
|
|
219
|
+
|
|
220
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
221
|
+
Promise.resolve().then(async () => {
|
|
222
|
+
if (process.argv.length !== 6 || process.argv[2] !== "connect") throw code("workbench_connect_arguments_invalid");
|
|
223
|
+
let input = "";
|
|
224
|
+
for await (const chunk of process.stdin) {
|
|
225
|
+
input += chunk;
|
|
226
|
+
if (Buffer.byteLength(input) > 4096) throw code("workbench_connect_input_invalid");
|
|
227
|
+
}
|
|
228
|
+
return connectAccount(process.argv[3], process.argv[4], process.argv[5], JSON.parse(input));
|
|
229
|
+
}).then((result) => process.stdout.write(`${JSON.stringify(result)}\n`), (error) => {
|
|
230
|
+
const safe = String(error?.code || error?.message || "");
|
|
231
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: /^[a-z][a-z0-9_]{1,127}$/.test(safe) ? safe : "workbench_connect_failed" })}\n`);
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
});
|
|
234
|
+
}
|