@awak-app/simy-cli 0.7.45 → 0.7.47
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/package.json +2 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/.mcp.json +11 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +60 -4
- package/src/codex-plugins/simy/hooks/measurement-collector.mjs +130 -0
- package/src/codex-plugins/simy/hooks/measurement-contract.mjs +51 -0
- package/src/codex-plugins/simy/hooks/measurement-judge.mjs +46 -0
- package/src/codex-plugins/simy/hooks/measurement-snapshot.mjs +89 -0
- package/src/codex-plugins/simy/hooks/measurement-store.mjs +132 -0
- package/src/codex-plugins/simy/native/app-tools.mjs +45 -0
- package/src/codex-plugins/simy/native/bridge.mjs +156 -0
- package/src/codex-plugins/simy/native/guidance-intent.mjs +30 -0
- package/src/codex-plugins/simy/native/launch +36 -0
- package/src/codex-plugins/simy/native/server.mjs +26 -0
- package/src/codex-plugins/simy/native/submission-receipt.mjs +9 -0
- package/src/delivery-hooks/command.js +6 -0
- package/src/delivery-hooks/measurement.js +149 -0
- package/src/delivery-hooks/supervisor.js +20 -2
- package/src/desktop-host.js +180 -12
- package/src/session-supervision/claude-sessions.js +25 -14
- package/src/session-supervision/claude-stream-owner.js +266 -0
- package/src/session-supervision/claude-stream-presentation.js +27 -0
- package/src/session-supervision/claude-stream-transport.js +188 -0
- package/src/session-supervision/codex-app-server.js +30 -8
- package/src/session-supervision/codex-app-tools-connection.js +30 -0
- package/src/session-supervision/controller-store.js +58 -8
- package/src/session-supervision/conversation-presentation.js +2 -0
- package/src/session-supervision/index.js +5 -1
- package/src/session-supervision/native-handoff.js +69 -13
- package/src/session-supervision/native-turn-observer.js +196 -0
- package/src/session-supervision/purpose-verification.js +18 -7
- package/src/session-supervision/session-guidance.js +160 -0
- package/src/sqm/claude-plugin.js +4 -0
- package/src/sqm/history-store.js +28 -1
- package/src/sqm/review-worker.js +5 -1
- package/src/web-api.js +5 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {dirname} from 'node:path';
|
|
2
|
+
import {realpath} from 'node:fs/promises';
|
|
3
|
+
import {openNativeBridge} from '../codex-plugins/simy/native/bridge.mjs';
|
|
4
|
+
import {guidanceAllowed, guidanceDigest, guidancePending, guidanceTarget, assertGuidanceIntent} from '../codex-plugins/simy/native/guidance-intent.mjs';
|
|
5
|
+
import {matchesNativeDelegation} from '../codex-plugins/simy/native/submission-receipt.mjs';
|
|
6
|
+
import {readRecord, updateRecord, hasPendingUserStop, managedBudgetReason, queuePurposeDiagnosis, advanceDeliveryControlRevision} from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
7
|
+
import {requireProviderLink} from '../provider-links.js';
|
|
8
|
+
|
|
9
|
+
const fail = code => Object.assign(new Error(code), {code});
|
|
10
|
+
const terminal = phase => ['observed', 'cancelled'].includes(phase);
|
|
11
|
+
function assertWritable(root, record, id, text) {
|
|
12
|
+
const intent = assertGuidanceIntent(record, id, text);
|
|
13
|
+
if (hasPendingUserStop(root, record) || managedBudgetReason(record.run)) throw fail('native_guidance_changed');
|
|
14
|
+
return intent;
|
|
15
|
+
}
|
|
16
|
+
export function sessionGuidancePresentation(candidate, record, now = Date.now()) {
|
|
17
|
+
const local = !candidate.remoteHost && candidate.teamRemote !== true && (!candidate.hostId || candidate.hostId === 'local');
|
|
18
|
+
const matched = record?.provider === candidate.provider && record?.session === candidate.session;
|
|
19
|
+
const observation = matched ? record.run?.managed?.nativeConnectionObservation : null;
|
|
20
|
+
const pending = matched ? record.run?.managed?.userGuidance : null;
|
|
21
|
+
const age = now - observation?.at;
|
|
22
|
+
const available = local && matched && candidate.isRunning === false && candidate.workState?.stale !== true &&
|
|
23
|
+
candidate.dispatchReadiness?.state !== 'blocked' && guidanceAllowed(record) &&
|
|
24
|
+
observation?.session === candidate.session && observation.state === 'connected' && observation.transport === 'codex_app_tools' && observation.guidanceAuthorization === true && age >= 0 && age < 30_000;
|
|
25
|
+
return {available: Boolean(available), state: pending?.phase || 'not_sent',
|
|
26
|
+
...(pending ? {requestId: pending.id} : {}), providerExecutionConfirmed: false,
|
|
27
|
+
...(available ? {target: guidanceTarget(record)} : {}),
|
|
28
|
+
reason: candidate.provider !== 'codex' ? 'direct_guidance_unsupported' : pending && !terminal(pending.phase) ? 'guidance_pending' : available ? null : 'original_connection_required'};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Read-only capability negotiation for the selected original conversation.
|
|
32
|
+
// A connection observation never resumes a writer or proves provider execution.
|
|
33
|
+
export async function observeSessionGuidanceConnection({root, record, signal, authorize = async () => {},
|
|
34
|
+
openClient = openNativeBridge, checkLink = requireProviderLink, now = Date.now}) {
|
|
35
|
+
if (!guidanceAllowed(record)) return record;
|
|
36
|
+
const target = JSON.stringify(guidanceTarget(record));
|
|
37
|
+
await authorize();
|
|
38
|
+
const link = await checkLink({root, provider: record.provider});
|
|
39
|
+
let client, observation;
|
|
40
|
+
try {
|
|
41
|
+
signal?.throwIfAborted();
|
|
42
|
+
client = await openClient({root: dirname(root), record, signal});
|
|
43
|
+
const thread = (await client?.request('thread/read', {threadId: record.session}))?.thread;
|
|
44
|
+
if (client?.transport !== 'codex_app_tools' || client.guidanceAuthorization !== true) throw fail('native_guidance_unavailable');
|
|
45
|
+
if (thread?.id !== record.session || thread.ephemeral || await realpath(thread.cwd) !== await realpath(record.cwd)) throw fail('native_tools_owner_mismatch');
|
|
46
|
+
if (thread.status?.type !== 'idle') throw fail('original_thread_not_idle');
|
|
47
|
+
observation = {state: 'connected', transport: client.transport, guidanceAuthorization: true};
|
|
48
|
+
} catch (error) {
|
|
49
|
+
observation = {state: 'unavailable', transport: 'codex_app_tools', guidanceAuthorization: false,
|
|
50
|
+
code: /^[a-zA-Z0-9_]{1,80}$/.test(error.code || '') ? error.code : 'native_connection_failed'};
|
|
51
|
+
} finally {await client?.close();}
|
|
52
|
+
await authorize();
|
|
53
|
+
await checkLink({root, provider: record.provider, expectedEpoch: link.epoch});
|
|
54
|
+
return updateRecord(root, record.provider, record.session, async fresh => {
|
|
55
|
+
if (!guidanceAllowed(fresh) || JSON.stringify(guidanceTarget(fresh)) !== target) return fresh;
|
|
56
|
+
// Recheck authority while holding the same task/intent lock used by sends.
|
|
57
|
+
await authorize();
|
|
58
|
+
await checkLink({root, provider: record.provider, expectedEpoch: link.epoch});
|
|
59
|
+
fresh.run.managed.nativeConnectionObservation = {...observation, session: record.session, at: now()};
|
|
60
|
+
return fresh;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A human explicitly submits a new instruction to the same loaded conversation.
|
|
65
|
+
// A durable, exact intent fences automatic work; the app bridge claims it once
|
|
66
|
+
// before dispatch. Unknown writes are reconciled only, never replayed.
|
|
67
|
+
export async function sendSessionGuidance({root, provider, session, requestId, expectedRunId, taskId, expectedRevision, expectedEpoch,
|
|
68
|
+
text, signal, authorize = async () => {}, openClient = openNativeBridge, checkLink = requireProviderLink, now = Date.now}) {
|
|
69
|
+
if (provider !== 'codex' || !/^[0-9a-f-]{36}$/i.test(session || '') || !/^[0-9a-f-]{36}$/i.test(requestId || '') ||
|
|
70
|
+
typeof text !== 'string' || !text.trim() || text.length > 4000) throw fail('native_guidance_invalid');
|
|
71
|
+
const prompt = `[SIMY user guidance ${requestId}]\n${text}`;
|
|
72
|
+
await authorize();
|
|
73
|
+
const link = await checkLink({root, provider});
|
|
74
|
+
let duplicate;
|
|
75
|
+
const record = await updateRecord(root, provider, session, r => {
|
|
76
|
+
const old = r?.run?.managed?.userGuidance;
|
|
77
|
+
if (old?.id === requestId) {
|
|
78
|
+
if (old.digest !== guidanceDigest(prompt)) throw fail('native_guidance_conflict');
|
|
79
|
+
duplicate = {state: old.phase, requestId}; return r;
|
|
80
|
+
}
|
|
81
|
+
if (!guidanceAllowed(r) || hasPendingUserStop(root, r) || managedBudgetReason(r.run) || r.run.id !== expectedRunId ||
|
|
82
|
+
r.run.managed.taskId !== taskId || guidanceTarget(r).revision !== expectedRevision || r.autoContinueEpoch !== expectedEpoch ||
|
|
83
|
+
r.run.managed.guidanceHistory?.some(item => item.id === requestId)) throw fail('native_guidance_changed');
|
|
84
|
+
if (old) r.run.managed.guidanceHistory = [...(r.run.managed.guidanceHistory || []), {id: old.id, digest: old.digest, phase: old.phase}];
|
|
85
|
+
r.run.managed.userGuidance = {id: requestId, text: prompt, digest: guidanceDigest(prompt), target: guidanceTarget(r), phase: 'prepared', createdAt: now()};
|
|
86
|
+
return r;
|
|
87
|
+
});
|
|
88
|
+
if (duplicate) return duplicate;
|
|
89
|
+
let client;
|
|
90
|
+
try {
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
client = await openClient({root: dirname(root), record, guidanceId: requestId, signal});
|
|
93
|
+
if (!client?.submitGuidance) throw fail('native_guidance_unavailable');
|
|
94
|
+
const thread = (await client.request('thread/read', {threadId: session}))?.thread;
|
|
95
|
+
if (thread?.id !== session || thread.ephemeral || thread.status?.type !== 'idle' || await realpath(thread.cwd) !== await realpath(record.cwd)) throw fail('native_guidance_unavailable');
|
|
96
|
+
await authorize(); await checkLink({root, provider, expectedEpoch: link.epoch});
|
|
97
|
+
await updateRecord(root, provider, session, r => {const intent = assertWritable(root, r, requestId, prompt); intent.phase = 'submitting'; return r;});
|
|
98
|
+
await client.submitGuidance({text: prompt, beforeSubmit: async () => {
|
|
99
|
+
await authorize(); await checkLink({root, provider, expectedEpoch: link.epoch});
|
|
100
|
+
assertWritable(root, await readRecord(root, provider, session), requestId, prompt);
|
|
101
|
+
}});
|
|
102
|
+
await updateRecord(root, provider, session, r => {
|
|
103
|
+
const intent = r?.run?.managed?.userGuidance;
|
|
104
|
+
if (intent?.id === requestId) {intent.phase = 'submitted'; intent.acceptedAt = now();}
|
|
105
|
+
return r;
|
|
106
|
+
});
|
|
107
|
+
return {state: 'submitted', requestId, providerExecutionConfirmed: false};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
await updateRecord(root, provider, session, r => {
|
|
110
|
+
const intent = r?.run?.managed?.userGuidance;
|
|
111
|
+
if (intent?.id === requestId && intent.phase !== 'observed') {
|
|
112
|
+
intent.phase = intent.bridgeAttemptedAt || ['native_submission_unknown', 'native_tools_closed'].includes(error.code) ? 'unknown' : 'cancelled';
|
|
113
|
+
intent.failedAt = now();
|
|
114
|
+
}
|
|
115
|
+
return r;
|
|
116
|
+
});
|
|
117
|
+
throw Object.assign(fail(error.code === 'native_guidance_changed' ? error.code : 'native_guidance_unconfirmed'), {cause: error});
|
|
118
|
+
} finally { await client?.close(); }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// A fresh bridge can read the exact original receipt after a host restart.
|
|
122
|
+
// No code path here invokes submitGuidance or retries a write.
|
|
123
|
+
export async function reconcileSessionGuidance({root, record, signal, authorize = async () => {}, openClient = openNativeBridge, now = Date.now}) {
|
|
124
|
+
const intent = record?.run?.managed?.userGuidance;
|
|
125
|
+
if (!guidancePending(record) || !intent.bridgeAttemptedAt) return false;
|
|
126
|
+
let client;
|
|
127
|
+
try {
|
|
128
|
+
await authorize();
|
|
129
|
+
signal?.throwIfAborted();
|
|
130
|
+
client = await openClient({root: dirname(root), record, guidanceId: intent.id, signal});
|
|
131
|
+
if (!client) return false;
|
|
132
|
+
let cursor;
|
|
133
|
+
for (let page = 0; page < 3; page++) {
|
|
134
|
+
const result = await client.request('thread/turns/list', {threadId: record.session, ...(cursor ? {cursor} : {})});
|
|
135
|
+
if (!Array.isArray(result?.data)) return false;
|
|
136
|
+
const turn = result.data.find(turn => matchesNativeDelegation(turn, record.session, intent.text));
|
|
137
|
+
if (turn) {
|
|
138
|
+
await authorize();
|
|
139
|
+
let inspectedNext = false;
|
|
140
|
+
await updateRecord(root, record.provider, record.session, r => {
|
|
141
|
+
if (r?.run?.id !== record.run.id || r.run.managed?.userGuidance?.id !== intent.id) return r;
|
|
142
|
+
const guide = r.run.managed.userGuidance;
|
|
143
|
+
guide.receiptObservedAt = now(); guide.turnId = turn.id;
|
|
144
|
+
// A receipt alone must not release the old automatic plan. If the
|
|
145
|
+
// controller cannot queue a fresh inspection (stop/budget/backoff),
|
|
146
|
+
// retain the send hold and let later read-only reconciliation retry.
|
|
147
|
+
inspectedNext = !hasPendingUserStop(root, r) && !r.run.stopReason && queuePurposeDiagnosis(r.run,
|
|
148
|
+
'Read the latest user guidance and current original conversation before choosing the next authorized action. Preserve corrections, stops, permissions, goals and budgets; do not repeat previous effects.',
|
|
149
|
+
`user-guidance:${intent.id}`, now(), 'user_guidance');
|
|
150
|
+
if (inspectedNext) {advanceDeliveryControlRevision(r.run); guide.phase = 'observed'; guide.observedAt = now();}
|
|
151
|
+
return r;
|
|
152
|
+
});
|
|
153
|
+
return inspectedNext;
|
|
154
|
+
}
|
|
155
|
+
if (!result.nextCursor || result.nextCursor === cursor) return false;
|
|
156
|
+
cursor = result.nextCursor;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
} finally { await client?.close(); }
|
|
160
|
+
}
|
package/src/sqm/claude-plugin.js
CHANGED
|
@@ -34,6 +34,8 @@ export async function claudePluginFiles() {
|
|
|
34
34
|
for (const name of ["loop-kernel.mjs", "loop-kernel/index.mjs", "loop-kernel/policy.json", "loop-kernel/pin.json", "loop-kernel/outcome-review.mjs", "loop-kernel/work-frontier.mjs", "loop-kernel/task-pool.mjs"]) {
|
|
35
35
|
files.set(`hooks/${name}`, await readFile(path.join(SOURCE, "hooks", name), "utf8"));
|
|
36
36
|
}
|
|
37
|
+
// The shared hook imports this standalone helper; no native bridge is needed.
|
|
38
|
+
files.set("native/guidance-intent.mjs", await readFile(path.join(SOURCE, "native/guidance-intent.mjs"), "utf8"));
|
|
37
39
|
const collector = await readFile(new URL("../cli-observability.js", import.meta.url), "utf8");
|
|
38
40
|
// Standalone hooks do not ship the CLI capability dependency graph. Freeze the
|
|
39
41
|
// maintained package version into their generated collector instead.
|
|
@@ -61,6 +63,8 @@ export async function claudePluginFiles() {
|
|
|
61
63
|
files.set("hooks/purpose-evidence.mjs", await readFile(path.join(SOURCE, "hooks/purpose-evidence.mjs"), "utf8"));
|
|
62
64
|
files.set("hooks/user-experience.mjs", await readFile(path.join(SOURCE, "hooks/user-experience.mjs"), "utf8"));
|
|
63
65
|
files.set("hooks/result-cycle.mjs", await readFile(path.join(SOURCE, "hooks/result-cycle.mjs"), "utf8"));
|
|
66
|
+
for (const name of ['measurement-contract', 'measurement-store', 'measurement-snapshot', 'measurement-judge', 'measurement-collector'])
|
|
67
|
+
files.set(`hooks/${name}.mjs`, await readFile(path.join(SOURCE, `hooks/${name}.mjs`), 'utf8'));
|
|
64
68
|
files.set("hooks/scene-reasoning.mjs", await readFile(path.join(SOURCE, "hooks/scene-reasoning.mjs"), "utf8"));
|
|
65
69
|
files.set("hooks/purpose-cycle.mjs", await readFile(path.join(SOURCE, "hooks/purpose-cycle.mjs"), "utf8"));
|
|
66
70
|
files.set("hooks/purpose-rule-policy.mjs", await readFile(path.join(SOURCE, "hooks/purpose-rule-policy.mjs"), "utf8"));
|
package/src/sqm/history-store.js
CHANGED
|
@@ -6,6 +6,9 @@ import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promise
|
|
|
6
6
|
import { basename, join } from "node:path";
|
|
7
7
|
import { formatSqmCoverage, sqmCoverageWarnings } from "./report.js";
|
|
8
8
|
import { defaultSessionRoot } from "../session-store.js";
|
|
9
|
+
import { recordMeasurementSqm } from '../codex-plugins/simy/hooks/measurement-collector.mjs';
|
|
10
|
+
import { measurementBinding, appendMeasurement, measurementCollectionEnabled } from '../codex-plugins/simy/hooks/measurement-store.mjs';
|
|
11
|
+
import { measurementDigest } from '../codex-plugins/simy/hooks/measurement-contract.mjs';
|
|
9
12
|
|
|
10
13
|
// Limit the desktop projection, never the durable history.
|
|
11
14
|
const DISPLAY_LIMIT = 100;
|
|
@@ -132,11 +135,35 @@ export function projectSqmHistory(items, live = { activeCount: 0, items: [] }) {
|
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
export function createSqmCheckRecorder({ root, scope, phase, cwd, baseBranch, providerSession, conversationProvider, onError = () => console.warn("Could not save the local SQM check history.") }) {
|
|
135
|
-
|
|
138
|
+
const historyEnabled = Boolean(scope?.webOrigin && scope?.accountId && scope?.organizationId);
|
|
136
139
|
const history = createSqmHistoryStore({ root });
|
|
137
140
|
const key = `sqm-check:${randomUUID()}`;
|
|
138
141
|
const startedAt = new Date().toISOString();
|
|
142
|
+
const measurementRoot = join(root || defaultSessionRoot(), 'delivery-hooks');
|
|
143
|
+
// Pin the current trace before the check starts. A new task in the same
|
|
144
|
+
// provider conversation cannot inherit the old task's late SQM result.
|
|
145
|
+
const measurement = providerSession && conversationProvider ? measurementBinding(measurementRoot, conversationProvider, providerSession).catch(() => null) : Promise.resolve(null);
|
|
139
146
|
return async (result, presentation = {}) => {
|
|
147
|
+
try {
|
|
148
|
+
const binding = await measurement;
|
|
149
|
+
if (binding && measurementCollectionEnabled(measurementRoot, conversationProvider, providerSession, binding.trace)) {
|
|
150
|
+
const context = { root: measurementRoot, provider: conversationProvider, session: providerSession, trace: binding.trace };
|
|
151
|
+
if (result.proof) await recordMeasurementSqm({ ...context, result });
|
|
152
|
+
else await appendMeasurement({ ...context, type: 'sqm_attempt', key,
|
|
153
|
+
data: { attempt: measurementDigest(key), phase: phase === 'min' ? 'min' : 'full',
|
|
154
|
+
outcome: result.status === 'unavailable' ? 'unavailable' : ['failed', 'preliminary_failed'].includes(result.status) ? 'failed' : ['passed', 'preliminary_passed'].includes(result.status) ? 'passed' : 'unknown',
|
|
155
|
+
candidate: result.repository_identity ? measurementDigest(result.repository_identity) : null,
|
|
156
|
+
diagnostic: result.diagnostic ? measurementDigest(result.diagnostic) : null } });
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
console.warn('Adoption measurement could not retain this SQM observation.');
|
|
160
|
+
try {
|
|
161
|
+
const binding = await measurement;
|
|
162
|
+
if (binding) await appendMeasurement({ root: measurementRoot, provider: conversationProvider, session: providerSession, trace: binding.trace,
|
|
163
|
+
type: 'gap', key: `sqm:${key}`, data: { reason: 'capture_failed' } });
|
|
164
|
+
} catch { /* The collection audit must report the damaged journal. */ }
|
|
165
|
+
}
|
|
166
|
+
if (!historyEnabled) return;
|
|
140
167
|
const proof = result.proof, identity = proof?.repository_identity || result.repository_identity || result.min_check?.repository_identity;
|
|
141
168
|
const repository = identity?.repository || result.repository;
|
|
142
169
|
const outcome = result.status === "stale" ? "stale" : proof?.outcome.status || result.min_check?.status || result.status;
|
package/src/sqm/review-worker.js
CHANGED
|
@@ -475,6 +475,8 @@ async function runSingleProviderReview(
|
|
|
475
475
|
) {
|
|
476
476
|
assertProvider(provider);
|
|
477
477
|
assertStructuredRequest(request);
|
|
478
|
+
// Probe the same filtered environment used by the actual provider process.
|
|
479
|
+
environment = safeReviewEnvironment(environment, provider);
|
|
478
480
|
if (signal?.aborted) throw new SqmLocalReviewError('Local analysis was cancelled.', { code: 'analysis_cancelled' });
|
|
479
481
|
const label = provider === "claude" ? "Claude Code" : "Codex";
|
|
480
482
|
const inspection = await inspectExecutor(provider);
|
|
@@ -621,7 +623,7 @@ function spawnReview(command, args, prompt, { spawnImpl, dispatchTransport, exec
|
|
|
621
623
|
child = dispatchTransport(() => {
|
|
622
624
|
try {
|
|
623
625
|
return spawnImpl(command, args, {
|
|
624
|
-
env:
|
|
626
|
+
env: environment, cwd, windowsHide: true, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"],
|
|
625
627
|
});
|
|
626
628
|
} catch { throw providerFailure("provider_spawn_failed", { model }); }
|
|
627
629
|
});
|
|
@@ -811,6 +813,8 @@ function providerSignInRequired(message) {
|
|
|
811
813
|
function safeReviewEnvironment(environment, provider) {
|
|
812
814
|
const allowed = [
|
|
813
815
|
"HOME", "PATH", "CODEX_HOME", "CLAUDE_CONFIG_DIR",
|
|
816
|
+
// Native credential lookup on macOS needs the local user identity too.
|
|
817
|
+
"USER", "LOGNAME",
|
|
814
818
|
"SystemRoot", "SYSTEMROOT", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", "TMP", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR",
|
|
815
819
|
"SSL_CERT_FILE", "SSL_CERT_DIR", "HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "NO_PROXY",
|
|
816
820
|
];
|
package/src/web-api.js
CHANGED
|
@@ -17,6 +17,9 @@ export function publicImageUrl(value) {
|
|
|
17
17
|
|
|
18
18
|
const APP_DEV_ORIGIN = "https://app-dev.simy.one";
|
|
19
19
|
const APP_DEV_GATEWAY_BASE_URL = "https://app.simy.one/api/local-cli-gateway/";
|
|
20
|
+
// An isolated dev build can explicitly select the dev gateway before a new
|
|
21
|
+
// route reaches stable. Never infer this endpoint or redirect a saved session.
|
|
22
|
+
const APP_DEV_LOCAL_GATEWAY_BASE_URL = "https://app-dev.simy.one/api/local-cli-gateway/";
|
|
20
23
|
|
|
21
24
|
export function resolveWebApiBaseUrl(webOrigin, requestedBaseUrl = null) {
|
|
22
25
|
const normalizedWebOrigin = new URL(webOrigin).origin;
|
|
@@ -36,9 +39,9 @@ export function resolveWebApiBaseUrl(webOrigin, requestedBaseUrl = null) {
|
|
|
36
39
|
if (requested.href === directBase) return directBase;
|
|
37
40
|
if (
|
|
38
41
|
normalizedWebOrigin === APP_DEV_ORIGIN &&
|
|
39
|
-
requested.href
|
|
42
|
+
[APP_DEV_GATEWAY_BASE_URL, APP_DEV_LOCAL_GATEWAY_BASE_URL].includes(requested.href)
|
|
40
43
|
) {
|
|
41
|
-
return
|
|
44
|
+
return requested.href;
|
|
42
45
|
}
|
|
43
46
|
throw new Error("SIMY Web provided an untrusted CLI API endpoint.");
|
|
44
47
|
}
|