@awak-app/simy-cli 0.6.11 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/package.json +2 -2
- package/src/agent.js +64 -43
- package/src/cli-contract.js +1 -1
- package/src/coding-run-recovery.js +5 -0
- package/src/connection-diagnostics.js +77 -0
- package/src/connection-discovery.js +161 -0
- package/src/index.js +21 -0
- package/src/launch-authorization.js +80 -0
- package/src/sqm/session.js +9 -2
package/README.md
CHANGED
|
@@ -816,3 +816,29 @@ Only summary metadata is retained, without analysis prompts, source inputs,
|
|
|
816
816
|
model responses or credentials. Historical summaries are not a replacement for
|
|
817
817
|
signed proof files or delivery acceptance. Records from versions that kept
|
|
818
818
|
analysis only in memory cannot be recovered after that process exits.
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
### Reusing a running service and launch recovery
|
|
822
|
+
|
|
823
|
+
`simy` and `simy --daemon` first inspect the private discovery record and verify
|
|
824
|
+
its live PID, instance, Web origin, CLI/API version and saved account,
|
|
825
|
+
organization and device. A compatible background service is reused: the entry
|
|
826
|
+
prints its existing address and opens SIMY Web (or its sign-in page). The local
|
|
827
|
+
interactive console is only created when this entry owns a new service.
|
|
828
|
+
SQM authorization also reuses that service and leaves it running when SQM exits.
|
|
829
|
+
Unreachable or incompatible live services produce diagnostics instead of a
|
|
830
|
+
second daemon. A stale record with a dead PID permits normal startup.
|
|
831
|
+
|
|
832
|
+
Web-led launches can omit their temporary challenge. The daemon requests a
|
|
833
|
+
credential for the existing queued run using its authenticated device. An
|
|
834
|
+
explicitly expired challenge can be renewed once; consumed, invalid or
|
|
835
|
+
cross-scope credentials cannot. Refresh the original run/device before retrying
|
|
836
|
+
a consumed credential because execution may have started before its ledger
|
|
837
|
+
status was uploaded. This recovery requires the companion Web
|
|
838
|
+
`/api/local-cli/challenges/issue` endpoint; existing valid challenges continue
|
|
839
|
+
to work against older Web versions.
|
|
840
|
+
|
|
841
|
+
Scope mismatch diagnostics identify the changed account, organization, device
|
|
842
|
+
or Web host and tell the caller to restore the original identity. Launch and
|
|
843
|
+
recovery operations do not switch identity automatically. The private
|
|
844
|
+
`/v1/connection` endpoint rejects browser requests and never returns tokens.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
|
|
44
44
|
"check:sqm": "node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
|
|
45
45
|
"check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js",
|
|
46
|
-
"check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/login-shell-path.js && node --check ./src/hook-runtime.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/session-supervision/automatic-continuation.js && node --check ./src/session-supervision/stop-notifications.js",
|
|
46
|
+
"check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/connection-diagnostics.js && node --check ./src/launch-authorization.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/login-shell-path.js && node --check ./src/hook-runtime.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/session-supervision/automatic-continuation.js && node --check ./src/session-supervision/stop-notifications.js",
|
|
47
47
|
"check:package": "node ./scripts/check-package-contents.js",
|
|
48
48
|
"build:macos-installer:dev": "bash packaging/build-macos-arm64.sh --unsigned-dev",
|
|
49
49
|
"build:macos-installer": "bash packaging/build-macos-arm64.sh --production",
|
package/src/agent.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assertConnectionScope, connectionScope, connectionDiagnostic } from "./connection-diagnostics.js";
|
|
2
|
+
import { authorizeLaunch } from "./launch-authorization.js";
|
|
1
3
|
import { normalizePurposeVerification, assertPurposeActor, preparePurposeVerification, verifyPurposeLocalTask, purposeSigningKeyPath, purposeVerificationCapability, purposeWorkspaceBusy } from "./purpose-local-proof.js";
|
|
2
4
|
import { startupRequirementChecks, summarizeStartupChecks } from "./startup-preflight.js";
|
|
3
5
|
import { BROWSER_HANDOFF_TRANSPORT, browserHandoffPage, decryptBrowserHandoff } from "./browser-handoff.js";
|
|
@@ -540,7 +542,7 @@ export async function startAgent({
|
|
|
540
542
|
})();
|
|
541
543
|
return organizationRefresh;
|
|
542
544
|
};
|
|
543
|
-
const requestDesktopLogin = (organizationId = null, { passive = false } = {}) => {
|
|
545
|
+
const requestDesktopLogin = (organizationId = null, { passive = false, reauthorize = false } = {}) => {
|
|
544
546
|
const authorization = authorizationWindow.snapshot();
|
|
545
547
|
if (authorization.state === "timed_out") {
|
|
546
548
|
if (passive) return null;
|
|
@@ -568,7 +570,7 @@ export async function startAgent({
|
|
|
568
570
|
refreshAvailableCapabilities();
|
|
569
571
|
notifyDesktopStatusChange();
|
|
570
572
|
} else if (isSessionValid(session, Date.now(), apiOrigin)) {
|
|
571
|
-
if (!enforceDesktopAccess || desktopAccess.snapshot().state !== 'signed_out') return null;
|
|
573
|
+
if (!reauthorize && (!enforceDesktopAccess || desktopAccess.snapshot().state !== 'signed_out')) return null;
|
|
572
574
|
if (!passive || !loginUrl) {
|
|
573
575
|
authNonce = randomBytes(16).toString("base64url");
|
|
574
576
|
loginUrl = null;
|
|
@@ -1027,14 +1029,17 @@ export async function startAgent({
|
|
|
1027
1029
|
);
|
|
1028
1030
|
};
|
|
1029
1031
|
|
|
1032
|
+
let connectionDiscovery = null;
|
|
1033
|
+
const launchingRuns = new Set();
|
|
1030
1034
|
const server = createServer(async (req, res) => {
|
|
1035
|
+
let reservedLaunch = null;
|
|
1031
1036
|
try {
|
|
1032
1037
|
const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
|
|
1033
1038
|
const localHandoff = url.pathname === "/v1/auth/browser-complete";
|
|
1034
1039
|
const localOrigin = `http://127.0.0.1:${port}`;
|
|
1035
1040
|
if (localHandoff ? (req.headers.origin !== localOrigin || req.headers.host !== `127.0.0.1:${port}`)
|
|
1036
1041
|
: !isRequestOriginAllowed(req.headers.origin, apiOrigin)) {
|
|
1037
|
-
json(res, 403,
|
|
1042
|
+
json(res, 403, connectionDiagnostic("connection_scope_changed", "web_origin"));
|
|
1038
1043
|
return;
|
|
1039
1044
|
}
|
|
1040
1045
|
applyCors(req, res, apiOrigin);
|
|
@@ -1050,7 +1055,7 @@ export async function startAgent({
|
|
|
1050
1055
|
return;
|
|
1051
1056
|
}
|
|
1052
1057
|
const agenticLoopPath = canonicalAgenticLoopPath(url.pathname);
|
|
1053
|
-
const desktopRecoveryRoute = ['/v1/health', '/v1/capabilities', '/v1/auth/complete', '/v1/auth/browser-complete'].includes(url.pathname) ||
|
|
1058
|
+
const desktopRecoveryRoute = ['/v1/health', '/v1/connection', '/v1/capabilities', '/v1/auth/complete', '/v1/auth/browser-complete'].includes(url.pathname) ||
|
|
1054
1059
|
req.method === 'POST' && (/^\/v1\/local-tasks\/[^/]+\/control$/.test(url.pathname) || /^\/v1\/agentic-loop\/[^/]+\/control$/.test(agenticLoopPath));
|
|
1055
1060
|
let requestAccessScope;
|
|
1056
1061
|
if (enforceDesktopAccess && !desktopRecoveryRoute) {
|
|
@@ -1075,6 +1080,28 @@ export async function startAgent({
|
|
|
1075
1080
|
res.once("finish", releaseUpdateWork);
|
|
1076
1081
|
res.once("close", releaseUpdateWork);
|
|
1077
1082
|
}
|
|
1083
|
+
if (["GET", "POST"].includes(req.method) && url.pathname === "/v1/connection") {
|
|
1084
|
+
// Private local entry points only. Browser origins must not read account
|
|
1085
|
+
// bindings, instance identifiers or the pending authorization nonce.
|
|
1086
|
+
if (req.headers.origin !== undefined || req.headers.host !== `127.0.0.1:${port}`) {
|
|
1087
|
+
json(res, 403, { error: "Local connection discovery only.", code: "local_connection_only" }); return;
|
|
1088
|
+
}
|
|
1089
|
+
let authorizationUrl;
|
|
1090
|
+
if (req.method === "POST") {
|
|
1091
|
+
const body = await readJson(req);
|
|
1092
|
+
if (!connectionDiscovery?.instanceId || body.instance_id !== connectionDiscovery.instanceId) {
|
|
1093
|
+
json(res, 409, connectionDiagnostic("connection_service_mismatch")); return;
|
|
1094
|
+
}
|
|
1095
|
+
try { assertConnectionScope(body.scope, connectionScope(session || reclaimableSession, apiOrigin)); }
|
|
1096
|
+
catch (error) { json(res, 409, { error: error.message, code: error.code, field: error.field, recovery: error.recovery }); return; }
|
|
1097
|
+
authorizationUrl = requestDesktopLogin(null, { reauthorize: body.reauthorize === true });
|
|
1098
|
+
} else authorizationUrl = requestDesktopLogin(null, { passive: true });
|
|
1099
|
+
json(res, 200, { ok: true, cli_version: CLI_VERSION, api_contract_version: CLI_API_CONTRACT_VERSION,
|
|
1100
|
+
daemon, web_origin: apiOrigin, instance_id: connectionDiscovery?.instanceId || null, pid: process.pid,
|
|
1101
|
+
scope: connectionScope(session || reclaimableSession, apiOrigin), session_valid: isSessionValid(session, Date.now(), apiOrigin),
|
|
1102
|
+
login_url: authorizationUrl });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1078
1105
|
if (req.method === "GET" && url.pathname === "/v1/health") {
|
|
1079
1106
|
const sessionValid = isSessionValid(session, Date.now(), apiOrigin);
|
|
1080
1107
|
const connection = connectionStatus.snapshot({ authenticated: sessionValid });
|
|
@@ -1987,25 +2014,23 @@ export async function startAgent({
|
|
|
1987
2014
|
return;
|
|
1988
2015
|
}
|
|
1989
2016
|
}
|
|
1990
|
-
const verified = await verifyLaunchChallenge({
|
|
1991
|
-
apiOrigin,
|
|
1992
|
-
apiBaseUrl: session.api_base_url,
|
|
1993
|
-
token: session.token,
|
|
1994
|
-
runId: String(body.run_id || ""),
|
|
1995
|
-
challenge: String(body.challenge || ""),
|
|
1996
|
-
});
|
|
1997
|
-
if (!verified.ok) {
|
|
1998
|
-
json(res, 403, { error: verified.error || "launch challenge rejected" });
|
|
1999
|
-
return;
|
|
2000
|
-
}
|
|
2001
2017
|
const runId = String(body.run_id || "");
|
|
2002
|
-
if (!runId) {
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
}
|
|
2018
|
+
if (!runId) { json(res, 400, { error: "run_id is required" }); return; }
|
|
2019
|
+
const launchSession = session;
|
|
2020
|
+
try { assertConnectionScope(body.connection_scope, connectionScope(launchSession, apiOrigin)); }
|
|
2021
|
+
catch (error) { json(res, 409, { error: error.message, code: error.code, field: error.field, recovery: error.recovery }); return; }
|
|
2022
|
+
if (launchingRuns.has(runId)) { json(res, 409, { error: "This launch is still being checked. Refresh its status before retrying.", code: "launch_in_progress" }); return; }
|
|
2023
|
+
launchingRuns.add(runId);
|
|
2024
|
+
reservedLaunch = runId;
|
|
2025
|
+
const verified = await authorizeLaunch({ apiOrigin, apiBaseUrl: launchSession.api_base_url,
|
|
2026
|
+
token: launchSession.token, runId, challenge: String(body.challenge || ""),
|
|
2027
|
+
fetchImpl: dependencies.fetch || globalThis.fetch });
|
|
2028
|
+
if (session !== launchSession || !isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2029
|
+
json(res, 409, changedLaunchSession(launchSession, session, apiOrigin)); return;
|
|
2030
|
+
}
|
|
2031
|
+
if (!verified.ok) { json(res, verified.status, connectionDiagnostic(verified.code)); return; }
|
|
2006
2032
|
if (registry.has(runId)) {
|
|
2007
|
-
json(res, 409, { error: "agentic loop run already exists" });
|
|
2008
|
-
return;
|
|
2033
|
+
json(res, 409, { error: "agentic loop run already exists" }); return;
|
|
2009
2034
|
}
|
|
2010
2035
|
let executionTarget;
|
|
2011
2036
|
try {
|
|
@@ -2069,6 +2094,10 @@ export async function startAgent({
|
|
|
2069
2094
|
});
|
|
2070
2095
|
return;
|
|
2071
2096
|
}
|
|
2097
|
+
if (session !== launchSession || !isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2098
|
+
await cleanupRunAttachments(stagedAttachments);
|
|
2099
|
+
json(res, 409, changedLaunchSession(launchSession, session, apiOrigin)); return;
|
|
2100
|
+
}
|
|
2072
2101
|
const run = createRun({
|
|
2073
2102
|
runId,
|
|
2074
2103
|
request: {
|
|
@@ -2136,6 +2165,9 @@ export async function startAgent({
|
|
|
2136
2165
|
throw error;
|
|
2137
2166
|
}
|
|
2138
2167
|
await authorizeDesktopWork(requestAccessScope);
|
|
2168
|
+
if (session !== launchSession || !isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2169
|
+
json(res, 409, changedLaunchSession(launchSession, session, apiOrigin)); return;
|
|
2170
|
+
}
|
|
2139
2171
|
void startLocalCodingRun(run, dependencies.runOptions);
|
|
2140
2172
|
json(res, 202, { ok: true, run_id: run.id });
|
|
2141
2173
|
return;
|
|
@@ -2216,7 +2248,7 @@ export async function startAgent({
|
|
|
2216
2248
|
json(res, 200, { recovery, run_id: run.id, resumed: false });
|
|
2217
2249
|
}
|
|
2218
2250
|
} catch (error) {
|
|
2219
|
-
json(res, 409, { error: error instanceof Error ? error.message : "Recovery could not be verified.", code: error.code || "recovery_rejected" });
|
|
2251
|
+
json(res, 409, { error: error instanceof Error ? error.message : "Recovery could not be verified.", code: error.code || "recovery_rejected", ...(error.field ? { field: error.field, recovery: error.recovery } : {}) });
|
|
2220
2252
|
}
|
|
2221
2253
|
return;
|
|
2222
2254
|
}
|
|
@@ -2449,13 +2481,16 @@ export async function startAgent({
|
|
|
2449
2481
|
}
|
|
2450
2482
|
if (String(err?.code || "").startsWith("purpose_verification_")) { json(res, err.status || 409, { error: err.message, code: err.code }); return; }
|
|
2451
2483
|
json(res, 500, { error: err instanceof Error ? err.message : "internal error" });
|
|
2484
|
+
} finally {
|
|
2485
|
+
// An aborted HTTP response must not leave a permanent launch reservation.
|
|
2486
|
+
// Keep the reservation until all asynchronous preparation has settled.
|
|
2487
|
+
if (reservedLaunch) launchingRuns.delete(reservedLaunch);
|
|
2452
2488
|
}
|
|
2453
2489
|
});
|
|
2454
2490
|
|
|
2455
2491
|
await new Promise((resolve) => server.listen(requestedPort, "127.0.0.1", resolve));
|
|
2456
2492
|
const address = server.address();
|
|
2457
2493
|
port = typeof address === "object" && address ? address.port : requestedPort;
|
|
2458
|
-
let connectionDiscovery = null;
|
|
2459
2494
|
try {
|
|
2460
2495
|
connectionDiscovery = await (
|
|
2461
2496
|
dependencies.publishConnectionDiscovery || publishConnectionDiscovery
|
|
@@ -3292,26 +3327,6 @@ function backendPreflightSummary(backend, inspection) {
|
|
|
3292
3327
|
return `${label} is not available. Install or update it: ${inspection.update_command}`;
|
|
3293
3328
|
}
|
|
3294
3329
|
|
|
3295
|
-
async function verifyLaunchChallenge({ apiOrigin, apiBaseUrl, token, runId, challenge }) {
|
|
3296
|
-
try {
|
|
3297
|
-
const response = await fetch(
|
|
3298
|
-
webApiUrl("challenges/verify", { webOrigin: apiOrigin, apiBaseUrl }),
|
|
3299
|
-
{
|
|
3300
|
-
method: "POST",
|
|
3301
|
-
headers: webApiHeaders(
|
|
3302
|
-
{ token },
|
|
3303
|
-
{ "Content-Type": "application/json" },
|
|
3304
|
-
),
|
|
3305
|
-
body: JSON.stringify({ run_id: runId, challenge }),
|
|
3306
|
-
},
|
|
3307
|
-
);
|
|
3308
|
-
if (response.ok) return { ok: true };
|
|
3309
|
-
const payload = await response.json().catch(() => null);
|
|
3310
|
-
return { ok: false, error: webApiErrorMessage(payload, response.status) };
|
|
3311
|
-
} catch (err) {
|
|
3312
|
-
return { ok: false, error: err instanceof Error ? err.message : "challenge check failed" };
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
3330
|
|
|
3316
3331
|
function canonicalAgenticLoopPath(pathname) {
|
|
3317
3332
|
return pathname.replace(/^\/v1\/coding-loop(?=\/|$)/, "/v1/agentic-loop");
|
|
@@ -3573,3 +3588,9 @@ function providerAuthenticationFailure(provider, status) {
|
|
|
3573
3588
|
}),
|
|
3574
3589
|
};
|
|
3575
3590
|
}
|
|
3591
|
+
|
|
3592
|
+
function changedLaunchSession(previous, current, origin) {
|
|
3593
|
+
try { assertConnectionScope(connectionScope(previous, origin), connectionScope(current, origin)); }
|
|
3594
|
+
catch (error) { return { error: error.message, code: error.code, field: error.field, recovery: error.recovery }; }
|
|
3595
|
+
return connectionDiagnostic("connection_session_changed");
|
|
3596
|
+
}
|
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.6.
|
|
16
|
+
export const CLI_VERSION = "0.6.12";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -91,6 +91,11 @@ export function assertRecoveryActor(run) {
|
|
|
91
91
|
if (saved[field] && saved[field] !== current[field]) {
|
|
92
92
|
const error = new Error(`The saved run belongs to another ${field}; restore its original authorized session.`);
|
|
93
93
|
error.code = "recovery_actor_mismatch";
|
|
94
|
+
error.field = field;
|
|
95
|
+
error.recovery = { action: "reconnect_cli", steps: [
|
|
96
|
+
"Select the original SIMY Web host, account and organization.",
|
|
97
|
+
"Reconnect the original device, refresh the saved run and retry recovery.",
|
|
98
|
+
] };
|
|
94
99
|
throw error;
|
|
95
100
|
}
|
|
96
101
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function connectionScope(session, webOrigin) {
|
|
2
|
+
return {
|
|
3
|
+
web_origin: webOrigin,
|
|
4
|
+
account_id: session?.account_id || session?.auth_user_id || null,
|
|
5
|
+
organization_id: session?.organization_id || session?.org_id || null,
|
|
6
|
+
device_id: session?.device_id || null,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function connectionDiagnostic(code, field = null) {
|
|
11
|
+
const scopeChanged = code === "connection_scope_changed";
|
|
12
|
+
const error = scopeChanged
|
|
13
|
+
? `SIMY ${field.replaceAll("_", " ")} changed. Restore the account, organization, device and Web host that authorized this run before reconnecting.`
|
|
14
|
+
: {
|
|
15
|
+
connection_service_mismatch:
|
|
16
|
+
"The discovered port belongs to a different service. Run simy doctor, then reconnect to the original SIMY Web host.",
|
|
17
|
+
connection_service_unavailable:
|
|
18
|
+
"A SIMY service is recorded but cannot be reached. Run simy doctor; reopen SIMY or restart the confirmed daemon before reconnecting.",
|
|
19
|
+
connection_service_not_background:
|
|
20
|
+
"The existing SIMY service runs in a foreground terminal. Keep that terminal open, or stop it when idle and start simy --daemon before reconnecting.",
|
|
21
|
+
connection_port_mismatch:
|
|
22
|
+
"The existing SIMY service uses a different port. Reconnect without --port to reuse it, or select its recorded port.",
|
|
23
|
+
connection_session_changed:
|
|
24
|
+
"SIMY sign-in changed while the launch was being authorized. Refresh this run on the original device before retrying.",
|
|
25
|
+
connection_service_incompatible:
|
|
26
|
+
"The existing SIMY service has an incompatible version or API. Run simy update --check; update or restart that service when its tasks are idle.",
|
|
27
|
+
connection_session_expired:
|
|
28
|
+
"SIMY sign-in expired. Open the verification link for the existing service and sign in to the original account and organization.",
|
|
29
|
+
launch_challenge_expired:
|
|
30
|
+
"The launch credential expired. Retry this same run to obtain a fresh credential.",
|
|
31
|
+
launch_challenge_consumed:
|
|
32
|
+
"This launch credential was already used. Refresh the run status on its original device before retrying; execution may already have started.",
|
|
33
|
+
launch_scope_mismatch:
|
|
34
|
+
"This run or credential is unavailable for the current account, organization or device. Reconnect to the service and identity that authorized the run.",
|
|
35
|
+
launch_challenge_invalid:
|
|
36
|
+
"The launch credential is invalid. Refresh this run in SIMY Web and reconnect to its original device.",
|
|
37
|
+
launch_service_incompatible:
|
|
38
|
+
"SIMY Web does not support launch-credential recovery yet. Update the Web service, refresh this run and retry on its original device.",
|
|
39
|
+
launch_service_unavailable:
|
|
40
|
+
"The SIMY launch service could not be reached. Check the connection and retry this same run.",
|
|
41
|
+
launch_run_not_queued:
|
|
42
|
+
"This run has already progressed or stopped. Refresh its status on the original device before starting anything again.",
|
|
43
|
+
}[code] ||
|
|
44
|
+
"SIMY could not authorize this launch. Refresh the run in SIMY Web and reconnect to its original device.";
|
|
45
|
+
return {
|
|
46
|
+
error,
|
|
47
|
+
code,
|
|
48
|
+
...(field ? { field } : {}),
|
|
49
|
+
recovery: {
|
|
50
|
+
action: "reconnect_cli",
|
|
51
|
+
steps:
|
|
52
|
+
scopeChanged || code === "launch_scope_mismatch"
|
|
53
|
+
? [
|
|
54
|
+
"Select the original SIMY Web host, account and organization.",
|
|
55
|
+
"Reconnect the original device, refresh the run and retry.",
|
|
56
|
+
]
|
|
57
|
+
: [error],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assertConnectionScope(expected, current) {
|
|
63
|
+
for (const field of [
|
|
64
|
+
"web_origin",
|
|
65
|
+
"account_id",
|
|
66
|
+
"organization_id",
|
|
67
|
+
"device_id",
|
|
68
|
+
]) {
|
|
69
|
+
if (expected?.[field] && expected[field] !== current?.[field]) {
|
|
70
|
+
const diagnostic = connectionDiagnostic(
|
|
71
|
+
"connection_scope_changed",
|
|
72
|
+
field,
|
|
73
|
+
);
|
|
74
|
+
throw Object.assign(new Error(diagnostic.error), diagnostic);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -3,6 +3,8 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
|
|
6
|
+
import { assertConnectionScope, connectionDiagnostic } from "./connection-diagnostics.js";
|
|
7
|
+
|
|
6
8
|
import { normalizeWebOrigin } from "./web-origin.js";
|
|
7
9
|
|
|
8
10
|
export function connectionDiscoveryPath(webOrigin, sessionRoot) {
|
|
@@ -47,3 +49,162 @@ export async function removeConnectionDiscovery(discovery) {
|
|
|
47
49
|
if (error?.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
|
|
48
50
|
}
|
|
49
51
|
}
|
|
52
|
+
|
|
53
|
+
// A private discovery file identifies a candidate; the live instance must prove
|
|
54
|
+
// it owns that record before an entry point can reuse it. Never send a token.
|
|
55
|
+
export async function findReusableConnection({
|
|
56
|
+
webOrigin,
|
|
57
|
+
sessionRoot,
|
|
58
|
+
cliVersion,
|
|
59
|
+
apiContractVersion,
|
|
60
|
+
expectedScope,
|
|
61
|
+
requestedPort = 0,
|
|
62
|
+
authorize = false,
|
|
63
|
+
reauthorize = false,
|
|
64
|
+
fetchImpl = fetch,
|
|
65
|
+
alive = (pid) => {
|
|
66
|
+
try {
|
|
67
|
+
process.kill(pid, 0);
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
}) {
|
|
74
|
+
const origin = normalizeWebOrigin(webOrigin);
|
|
75
|
+
let record;
|
|
76
|
+
try {
|
|
77
|
+
record = JSON.parse(
|
|
78
|
+
await readFile(connectionDiscoveryPath(origin, sessionRoot), "utf8"),
|
|
79
|
+
);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error.code === "ENOENT") return null;
|
|
82
|
+
throw connectionError("connection_service_mismatch");
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
record?.schema_version !== 1 ||
|
|
86
|
+
record.web_origin !== origin ||
|
|
87
|
+
!Number.isInteger(record.port) ||
|
|
88
|
+
record.port < 1 ||
|
|
89
|
+
record.port > 65535 ||
|
|
90
|
+
!Number.isInteger(record.pid) ||
|
|
91
|
+
record.pid <= 0 ||
|
|
92
|
+
typeof record.instance_id !== "string" ||
|
|
93
|
+
!record.instance_id
|
|
94
|
+
) {
|
|
95
|
+
throw connectionError("connection_service_mismatch");
|
|
96
|
+
}
|
|
97
|
+
if (!alive(record.pid)) return null;
|
|
98
|
+
let response, health;
|
|
99
|
+
try {
|
|
100
|
+
response = await fetchImpl(
|
|
101
|
+
`http://127.0.0.1:${record.port}/v1/connection`,
|
|
102
|
+
{
|
|
103
|
+
signal: AbortSignal.timeout(1500),
|
|
104
|
+
redirect: "error",
|
|
105
|
+
cache: "no-store",
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
health = response.ok ? await response.json() : null;
|
|
109
|
+
} catch {
|
|
110
|
+
throw connectionError("connection_service_unavailable");
|
|
111
|
+
}
|
|
112
|
+
if (response.status === 404)
|
|
113
|
+
throw connectionError("connection_service_incompatible");
|
|
114
|
+
if (
|
|
115
|
+
!health?.ok ||
|
|
116
|
+
health.instance_id !== record.instance_id ||
|
|
117
|
+
health.pid !== record.pid ||
|
|
118
|
+
health.web_origin !== origin
|
|
119
|
+
) {
|
|
120
|
+
throw connectionError("connection_service_mismatch");
|
|
121
|
+
}
|
|
122
|
+
if (health.daemon !== true)
|
|
123
|
+
throw connectionError("connection_service_not_background");
|
|
124
|
+
if (requestedPort && requestedPort !== record.port)
|
|
125
|
+
throw connectionError("connection_port_mismatch");
|
|
126
|
+
if (
|
|
127
|
+
!compatibleServiceVersion(health.cli_version, cliVersion) ||
|
|
128
|
+
health.api_contract_version !== apiContractVersion
|
|
129
|
+
)
|
|
130
|
+
throw connectionError("connection_service_incompatible");
|
|
131
|
+
assertConnectionScope(expectedScope, health.scope);
|
|
132
|
+
if (authorize && (reauthorize || !health.session_valid)) {
|
|
133
|
+
try {
|
|
134
|
+
response = await fetchImpl(
|
|
135
|
+
`http://127.0.0.1:${record.port}/v1/connection`,
|
|
136
|
+
{
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "Content-Type": "application/json" },
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
instance_id: record.instance_id,
|
|
141
|
+
scope: health.scope,
|
|
142
|
+
reauthorize,
|
|
143
|
+
}),
|
|
144
|
+
signal: AbortSignal.timeout(1500),
|
|
145
|
+
redirect: "error",
|
|
146
|
+
cache: "no-store",
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
health = await response.json().catch(() => null);
|
|
150
|
+
} catch {
|
|
151
|
+
throw connectionError("connection_service_unavailable");
|
|
152
|
+
}
|
|
153
|
+
if (!response.ok && health?.code === "connection_scope_changed" &&
|
|
154
|
+
["web_origin", "account_id", "organization_id", "device_id"].includes(health.field)) {
|
|
155
|
+
const diagnostic = connectionDiagnostic(health.code, health.field);
|
|
156
|
+
throw Object.assign(new Error(diagnostic.error), diagnostic);
|
|
157
|
+
}
|
|
158
|
+
if (!response.ok) throw connectionError("connection_service_mismatch");
|
|
159
|
+
if (
|
|
160
|
+
!health?.ok ||
|
|
161
|
+
health.instance_id !== record.instance_id ||
|
|
162
|
+
health.pid !== record.pid ||
|
|
163
|
+
health.web_origin !== origin
|
|
164
|
+
) {
|
|
165
|
+
throw connectionError("connection_service_mismatch");
|
|
166
|
+
}
|
|
167
|
+
assertConnectionScope(expectedScope, health.scope);
|
|
168
|
+
}
|
|
169
|
+
if (health.login_url !== null) {
|
|
170
|
+
let url;
|
|
171
|
+
try {
|
|
172
|
+
url = new URL(health.login_url);
|
|
173
|
+
} catch {
|
|
174
|
+
throw connectionError("connection_service_mismatch");
|
|
175
|
+
}
|
|
176
|
+
if (
|
|
177
|
+
url.origin !== origin ||
|
|
178
|
+
url.pathname !== "/local-cli/connect" ||
|
|
179
|
+
url.searchParams.get("port") !== String(record.port)
|
|
180
|
+
) {
|
|
181
|
+
throw connectionError("connection_service_mismatch");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
port: record.port,
|
|
186
|
+
webOrigin: origin,
|
|
187
|
+
loginUrl: health.login_url,
|
|
188
|
+
sessionValid: health.session_valid === true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function connectionError(code) {
|
|
193
|
+
const diagnostic = connectionDiagnostic(code);
|
|
194
|
+
return Object.assign(new Error(diagnostic.error), diagnostic);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function compatibleServiceVersion(running, requested) {
|
|
198
|
+
const version = (value) =>
|
|
199
|
+
typeof value === "string"
|
|
200
|
+
? /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.exec(value)
|
|
201
|
+
: null;
|
|
202
|
+
const existing = version(running),
|
|
203
|
+
target = version(requested);
|
|
204
|
+
return Boolean(
|
|
205
|
+
existing &&
|
|
206
|
+
target &&
|
|
207
|
+
existing[1] === target[1] &&
|
|
208
|
+
(existing[1] !== "0" || existing[2] === target[2]),
|
|
209
|
+
);
|
|
210
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,10 @@ import { createInterface } from "node:readline/promises";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
|
+
import { findReusableConnection } from "./connection-discovery.js";
|
|
10
|
+
import { connectionScope } from "./connection-diagnostics.js";
|
|
11
|
+
import { readSession } from "./session-store.js";
|
|
12
|
+
import { CLI_API_CONTRACT_VERSION } from "./cli-contract.js";
|
|
9
13
|
import { startAgent } from "./agent.js";
|
|
10
14
|
import { createRuntimeDiagnostics, captureRuntimeOutput } from "./runtime-diagnostics.js";
|
|
11
15
|
import {
|
|
@@ -165,6 +169,23 @@ async function runLocalAgent() {
|
|
|
165
169
|
const webOrigin = configuredWebOrigin;
|
|
166
170
|
const sessionRoot = process.env.SIMY_HOME?.trim() || join(homedir(), ".simy");
|
|
167
171
|
|
|
172
|
+
try {
|
|
173
|
+
const existing = await findReusableConnection({ webOrigin, sessionRoot, cliVersion: packageInfo.version,
|
|
174
|
+
apiContractVersion: CLI_API_CONTRACT_VERSION, requestedPort: port, authorize: true,
|
|
175
|
+
expectedScope: connectionScope(await readSession(webOrigin, sessionRoot), webOrigin) });
|
|
176
|
+
if (existing) {
|
|
177
|
+
console.log(`Connected to existing SIMY service at http://127.0.0.1:${existing.port}`);
|
|
178
|
+
const url = existing.loginUrl || webOrigin;
|
|
179
|
+
console.log(`Continue in SIMY Web: ${url}`);
|
|
180
|
+
if (!args.has('--no-open') && process.env.SIMY_DAEMON_CHILD !== '1') await openAuthorizationUrl(url);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(error.message);
|
|
185
|
+
process.exitCode = 2;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
168
189
|
try {
|
|
169
190
|
await prepareDaemonWorkflows({ argv, daemon });
|
|
170
191
|
} catch (error) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { webApiHeaders, webApiUrl } from "./web-api.js";
|
|
2
|
+
|
|
3
|
+
// Only an explicitly expired credential is renewable. A consumed or rejected
|
|
4
|
+
// credential may represent an execution that already started; never replay it.
|
|
5
|
+
export async function authorizeLaunch({
|
|
6
|
+
apiOrigin,
|
|
7
|
+
apiBaseUrl,
|
|
8
|
+
token,
|
|
9
|
+
runId,
|
|
10
|
+
challenge,
|
|
11
|
+
fetchImpl = fetch,
|
|
12
|
+
}) {
|
|
13
|
+
const post = async (path, body) => {
|
|
14
|
+
const response = await fetchImpl(
|
|
15
|
+
webApiUrl(path, { webOrigin: apiOrigin, apiBaseUrl }),
|
|
16
|
+
{
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: webApiHeaders(
|
|
19
|
+
{ token },
|
|
20
|
+
{ "Content-Type": "application/json" },
|
|
21
|
+
),
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
redirect: "error",
|
|
24
|
+
signal: AbortSignal.timeout(10_000),
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
return { response, payload: await response.json().catch(() => null) };
|
|
28
|
+
};
|
|
29
|
+
const rejected = ({ response, payload }) => ({
|
|
30
|
+
ok: false,
|
|
31
|
+
status: response.status >= 500 ? 502 : response.status === 401 ? 401 : 403,
|
|
32
|
+
code:
|
|
33
|
+
typeof payload?.code === "string"
|
|
34
|
+
? payload.code
|
|
35
|
+
: response.status >= 500
|
|
36
|
+
? "launch_service_unavailable"
|
|
37
|
+
: response.status === 401
|
|
38
|
+
? "connection_session_expired"
|
|
39
|
+
: response.status === 404
|
|
40
|
+
? "launch_service_incompatible"
|
|
41
|
+
: "launch_challenge_invalid",
|
|
42
|
+
});
|
|
43
|
+
const verificationOutcome = (result) => {
|
|
44
|
+
if (!result.response.ok) return rejected(result);
|
|
45
|
+
return result.payload?.ok === true
|
|
46
|
+
? { ok: true }
|
|
47
|
+
: { ok: false, status: 502, code: "launch_service_unavailable" };
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
if (challenge) {
|
|
51
|
+
const verified = await post("challenges/verify", {
|
|
52
|
+
run_id: runId,
|
|
53
|
+
challenge,
|
|
54
|
+
});
|
|
55
|
+
if (verified.response.ok) return verificationOutcome(verified);
|
|
56
|
+
if (verified.payload?.code !== "launch_challenge_expired")
|
|
57
|
+
return rejected(verified);
|
|
58
|
+
}
|
|
59
|
+
const issued = await post("challenges/issue", {
|
|
60
|
+
run_id: runId,
|
|
61
|
+
...(challenge ? { previous_challenge: challenge } : {}),
|
|
62
|
+
});
|
|
63
|
+
if (!issued.response.ok) return rejected(issued);
|
|
64
|
+
if (
|
|
65
|
+
typeof issued.payload?.challenge !== "string" ||
|
|
66
|
+
!issued.payload.challenge ||
|
|
67
|
+
!Number.isFinite(Date.parse(issued.payload.expires_at)) ||
|
|
68
|
+
Date.parse(issued.payload.expires_at) <= Date.now()
|
|
69
|
+
) {
|
|
70
|
+
return { ok: false, status: 502, code: "launch_service_unavailable" };
|
|
71
|
+
}
|
|
72
|
+
const verified = await post("challenges/verify", {
|
|
73
|
+
run_id: runId,
|
|
74
|
+
challenge: issued.payload.challenge,
|
|
75
|
+
});
|
|
76
|
+
return verificationOutcome(verified);
|
|
77
|
+
} catch {
|
|
78
|
+
return { ok: false, status: 502, code: "launch_service_unavailable" };
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/sqm/session.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { findReusableConnection } from "../connection-discovery.js";
|
|
2
|
+
import { connectionScope } from "../connection-diagnostics.js";
|
|
3
|
+
import { CLI_VERSION, CLI_API_CONTRACT_VERSION } from "../cli-contract.js";
|
|
1
4
|
import { startAgent } from "../agent.js";
|
|
2
5
|
import { openAuthorizationUrl } from "../browser.js";
|
|
3
6
|
import { isSessionValid, readSession } from "../session-store.js";
|
|
@@ -16,6 +19,7 @@ export async function ensureSqmSession(
|
|
|
16
19
|
readSessionImpl = readSession,
|
|
17
20
|
isSessionValidImpl = isSessionValid,
|
|
18
21
|
startAgentImpl = startAgent,
|
|
22
|
+
findConnectionImpl = findReusableConnection,
|
|
19
23
|
openAuthorizationUrlImpl = openAuthorizationUrl,
|
|
20
24
|
createAgentShutdownImpl = createAgentShutdown,
|
|
21
25
|
expectedScope,
|
|
@@ -31,8 +35,11 @@ export async function ensureSqmSession(
|
|
|
31
35
|
return session;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
const
|
|
35
|
-
|
|
38
|
+
const existing = await findConnectionImpl({ webOrigin, cliVersion: CLI_VERSION,
|
|
39
|
+
apiContractVersion: CLI_API_CONTRACT_VERSION, expectedScope: connectionScope(session, webOrigin),
|
|
40
|
+
authorize: true, reauthorize: session?.token === rejectedToken && Boolean(rejectedToken) });
|
|
41
|
+
const agent = existing || await startAgentImpl({ requestedPort: 0, webOrigin, quiet: true });
|
|
42
|
+
const shutdown = existing ? async () => {} : createAgentShutdownImpl(agent);
|
|
36
43
|
try {
|
|
37
44
|
const loginUrl = agent.loginUrl;
|
|
38
45
|
if (!loginUrl) throw new Error("SIMY could not create an SQM authorization session.");
|