Skip to content

Commit aee2796

Browse files
committed
Merge remote-tracking branch 'upstream/main' into codex/goal-artifact-lifecycle-projection
Signed-off-by: song <liusongstep@gmail.com> # Conflicts: # loopx/semantics/inventory_v0.json
2 parents 499c85a + 4aaad69 commit aee2796

159 files changed

Lines changed: 12484 additions & 1808 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitattributes‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
# Vite bundles may retain whitespace-only lines inside generated template literals.
22
loopx/web/chat/assets/*.js whitespace=-blank-at-eol
3+
4+
# Preserve the immutable archive's bytes even with core.autocrlf=true.
5+
benchmark/deepswe-gptxhigh-v1/** text eol=lf

‎.github/workflows/full-public-smokes.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ jobs:
7373
- name: Install smoke runtime dependencies
7474
run: python -m pip install --disable-pip-version-check "jsonschema>=4.23,<5"
7575

76+
- name: Install locked TypeScript parser for semantic production checks
77+
run: npm ci --ignore-scripts
78+
7679
- name: Preview full-public shard
7780
run: |
7881
python3 examples/run-smokes.py \

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ goals/**/ACTIVE_GOAL_STATE.md
3737
goals/**/ACTIVE_GOAL_STATE.md.lock
3838
/runtime/
3939
logs/
40+
41+
# Legacy semantic census reports are derived locally, never repository authority.
42+
/loopx/semantics/inventory_v0.json

‎AGENTS.md‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,22 @@ broader actor lifecycle or authority model than the implementation provides.
222222

223223
## Engineering Quality And Right-Sized Scope
224224

225+
### Source-Checkout Python Entry Points
226+
227+
Run source development and validation from the intended worktree root with
228+
`uv run --extra test python ...` or `uv run --extra test loopx ...`. Use
229+
`uv sync --extra test` to prepare the project environment. An explicitly
230+
activated compatible environment with the checkout installed remains valid.
231+
Check `sys.executable` and `loopx.__file__` when interpreter or source provenance
232+
is uncertain; a global `loopx` may point to another release snapshot.
233+
234+
Keep Python subprocesses on the selected interpreter (`sys.executable`), and
235+
keep bootstrap interpreter discovery, supported-version declarations, CI
236+
version coverage, and version-specific fixtures intact. Do not replace those
237+
with a nested `uv run`, rewrite historical execution receipts, or commit a
238+
generated `uv.lock` as part of an unrelated change. See the testing and quality
239+
guide for the validation layers and the source-checkout environment boundary.
240+
225241
### Refactor Real-Path Validation
226242

227243
Before delivering a refactor, validate the affected production entrypoint and
@@ -456,6 +472,10 @@ Use this classification when cleaning or reviewing benchmark-related changes:
456472
provider-neutral capability contract.
457473
- Keep benchmark-native runners, adapters, ledgers, scoring reducers, and dated
458474
experiment packets outside the active product surface. Historical versions
459-
belong under `deprecate/benchmark-legacy/` and are not part of active CI.
475+
follow the canonical archive placement rules in `benchmark/README.md`:
476+
retired implementations and dated packets belong under
477+
`deprecate/benchmark-legacy/`; explicitly identified immutable experiment
478+
snapshots may remain under `benchmark/` only under that document's conditions.
479+
Neither category is part of active CI benchmark execution.
460480
- Add a new active benchmark smoke only when it protects a stable toolkit
461481
behavior; experiment-specific validation belongs with the research workspace.

‎apps/presentation/dashboard/README.md‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,21 @@ LoopX services are already running separately. Vite proxies the default
147147
The full-stack launcher needs a Python 3.11+ interpreter for the status and
148148
Chat services. It honors `LOOPX_PYTHON` first, then the Python recorded by the
149149
LoopX installer in `.loopx-python`, then the repository `.venv`,
150-
`python3.13`/`python3.12`/`python3.11` on `PATH`, and common Homebrew locations.
151-
If your default `python3` is older, point it at an existing interpreter:
150+
versioned interpreters discovered on `PATH` in descending numeric order, the
151+
unversioned `python3`, and common Homebrew locations. Every discovered executable
152+
must pass the Python compatibility probe; there is no fixed minor-version list.
153+
Prepare the project environment and launch from the repository root:
152154

153155
```bash
154-
LOOPX_PYTHON=/path/to/python3.12 npm run dev
156+
uv sync --extra test
157+
uv run --extra test bash scripts/dashboard-dev.sh
155158
```
156159

160+
An explicit `LOOPX_PYTHON` or a valid installer-recorded interpreter still takes
161+
precedence. To select the project environment explicitly after `uv sync`, set
162+
`LOOPX_PYTHON` to the absolute path of `.venv/bin/python`. The launcher continues
163+
to support existing compatible Python installations without requiring uv.
164+
157165
Both the root dashboard and the packaged `/chat/` route expose the same
158166
installable PWA manifest and icons. The default `loopx dashboard` command opens
159167
`/chat/`; its manifest therefore scopes the installed app to `/chat/`. This is

‎apps/presentation/dashboard/src/data/chat.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,55 @@ export type MachineConfigurationPreview = z.infer<typeof machineConfigurationPre
13711371
export type MachineConfigurationTransaction = z.infer<typeof machineConfigurationTransactionSchema>;
13721372
export type MachineConfigurationRollbackPlan = z.infer<typeof machineConfigurationRollbackPlanSchema>;
13731373

1374+
// The operator credential readback is redacted by construction: the key field
1375+
// carries a fingerprint and never a value, so this schema has no place to put
1376+
// one even if a future server tried to send it.
1377+
export const operatorCredentialFieldSchema = z.object({
1378+
configured: z.boolean(),
1379+
source: z.enum(["machine_store", "service_environment", "unset"]),
1380+
env_var: z.string().optional(),
1381+
fingerprint: z.string().nullable().optional(),
1382+
value: z.string().nullable().optional(),
1383+
blocked_by: z.string().optional(),
1384+
});
1385+
1386+
export const operatorCredentialSchema = z.object({
1387+
ok: z.literal(true),
1388+
// The chat route returns the same versioned projection the CLI prints, so the
1389+
// browser and the terminal cannot drift into two spellings of one readback.
1390+
schema_version: z.literal("operator_provider_credential_projection_v0"),
1391+
action: z.string().optional(),
1392+
store_ref: z.string(),
1393+
store_revision: z.string(),
1394+
record_present: z.boolean(),
1395+
status: z.enum(["configured", "absent", "invalid"]),
1396+
repair: z.string(),
1397+
provider_key: operatorCredentialFieldSchema,
1398+
base_url: operatorCredentialFieldSchema,
1399+
});
1400+
1401+
export type OperatorCredential = z.infer<typeof operatorCredentialSchema>;
1402+
1403+
export async function fetchOperatorCredential() {
1404+
return operatorCredentialSchema.parse(
1405+
await requestJson<unknown>("/api/chat/operator-credential"),
1406+
);
1407+
}
1408+
1409+
export async function writeOperatorCredential(update: {
1410+
provider_key?: string;
1411+
base_url?: string;
1412+
clear_provider_key?: boolean;
1413+
clear_base_url?: boolean;
1414+
}) {
1415+
return operatorCredentialSchema.parse(
1416+
await requestJson<unknown>("/api/chat/operator-credential", {
1417+
method: "POST",
1418+
body: JSON.stringify(update),
1419+
}),
1420+
);
1421+
}
1422+
13741423
export async function fetchMachineConfiguration() {
13751424
return machineConfigurationInspectionSchema.parse(
13761425
await requestJson<unknown>("/api/chat/machine-configuration"),

‎apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,26 @@ const en = {
801801
"machine.editorMode": "Editor mode",
802802
"machine.liveDefault": "Live default; Goal override wins",
803803
"machine.liveDefaultDescription": "Goals without an explicit override read the current machine policy at the capability’s next decision point. Changes and removal affect those existing Goals immediately; an explicit Goal override stays pinned.",
804+
"machine.credentialTitle": "Operator model credential",
805+
"machine.credentialDescription": "The key and endpoint the steward channel and the managed host authenticate with on this machine. The key is stored in its own owner-only file, never in the machine configuration that is projected here, and it is never read back — only its fingerprint is.",
806+
"machine.credentialApiKey": "API key",
807+
"machine.credentialApiKeyPlaceholder": "Paste a key to store it; leave blank to keep the stored one",
808+
"machine.credentialBaseUrl": "Endpoint base URL",
809+
"machine.credentialBaseUrlPlaceholder": "https://endpoint.example/v1 (blank keeps the endpoint default)",
810+
"machine.credentialStore": "Store credential",
811+
"machine.credentialClearKey": "Clear stored key",
812+
"machine.credentialClearUrl": "Clear stored endpoint",
813+
"machine.credentialConfigured": "configured",
814+
"machine.credentialAbsent": "not configured",
815+
"machine.credentialInvalid": "unreadable — repair required",
816+
"machine.credentialSourceMachine": "this machine's stored credential",
817+
"machine.credentialSourceEnvironment": "the service environment",
818+
"machine.credentialSourceUnset": "no source",
819+
"machine.credentialFingerprint": "fingerprint",
820+
"machine.credentialStored": "Credential stored. The next turn uses it; no restart is needed.",
821+
"machine.credentialCleared": "Stored credential cleared.",
822+
"machine.credentialError": "The credential could not be stored.",
823+
"machine.credentialBoundary": "Storing a credential grants no authority: it does not select an executor, model, or reasoning effort.",
804824
"machine.genericNamespaceDescription": "Edit this registered namespace as JSON. LoopX validates it with the capability-owned schema before previewing any write.",
805825
"machine.jsonConfiguration": "Namespace configuration (JSON)",
806826
"machine.jsonConfigurationHelp": "Only this namespace is updated. Other machine configuration is preserved, and Apply remains locked to the reviewed preview revision.",
@@ -1809,6 +1829,26 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
18091829
"machine.editorMode": "编辑模式",
18101830
"machine.liveDefault": "实时默认值;Goal 显式覆盖优先",
18111831
"machine.liveDefaultDescription": "没有显式覆盖的 Goal 会在该能力下一次决策时读取当前机器策略。修改或移除策略会立即影响这些已有 Goal;显式 Goal 覆盖保持固定。",
1832+
"machine.credentialTitle": "操作者模型凭据",
1833+
"machine.credentialDescription": "管家通道与托管宿主在本机认证用的 key 与 endpoint。key 单独存放于仅属主可读的文件,不会进入这里展示的机器配置,也不会被回读——回读的是它的指纹。",
1834+
"machine.credentialApiKey": "API key",
1835+
"machine.credentialApiKeyPlaceholder": "粘贴 key 以保存;留空则保留已存的 key",
1836+
"machine.credentialBaseUrl": "Endpoint base URL",
1837+
"machine.credentialBaseUrlPlaceholder": "https://endpoint.example/v1(留空则使用 endpoint 默认值)",
1838+
"machine.credentialStore": "保存凭据",
1839+
"machine.credentialClearKey": "清除已存 key",
1840+
"machine.credentialClearUrl": "清除已存 endpoint",
1841+
"machine.credentialConfigured": "已配置",
1842+
"machine.credentialAbsent": "未配置",
1843+
"machine.credentialInvalid": "无法读取——需要修复",
1844+
"machine.credentialSourceMachine": "本机已存凭据",
1845+
"machine.credentialSourceEnvironment": "服务环境变量",
1846+
"machine.credentialSourceUnset": "无来源",
1847+
"machine.credentialFingerprint": "指纹",
1848+
"machine.credentialStored": "凭据已保存。下一轮即生效,无需重启。",
1849+
"machine.credentialCleared": "已清除本机存储的凭据。",
1850+
"machine.credentialError": "凭据保存失败。",
1851+
"machine.credentialBoundary": "保存凭据不授予任何权限:它不会选择执行器、模型或推理强度。",
18121852
"machine.genericNamespaceDescription": "使用 JSON 编辑这个已注册 Namespace。LoopX 会先按 capability 自己拥有的 schema 校验,再允许预览写入。",
18131853
"machine.jsonConfiguration": "Namespace 配置(JSON)",
18141854
"machine.jsonConfigurationHelp": "只更新当前 Namespace;其他机器配置会保留,Apply 仍锁定到已审阅的 Preview Revision。",

‎apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { withReportScheduleTimezone } from "./periodic-report-schedule-field";
2121
import { localizeCapability, localizedCapabilityFieldCopy } from "./capability-localization";
2222
import { canEditCapability, CapabilityCatalogNavigation, CapabilityConfigurationSummary, CapabilityDetailHeader, CapabilityEditorStatus, orderCapabilitiesForPresentation } from "./capability-workbench";
2323
import { useWorkspaceI18n } from "./i18n";
24+
import { OperatorCredentialSettings } from "./operator-credential-settings";
2425

2526
type CapabilityDescriptor = CapabilityConfigurationCatalog["capabilities"][number];
2627
type EditorMode = "guided" | "json";
@@ -300,6 +301,8 @@ export function MachineConfigurationSettings() {
300301
</section>
301302
) : null}
302303

304+
<OperatorCredentialSettings />
305+
303306
<div className="personal-capability-layout">
304307
<CapabilityCatalogNavigation capabilities={capabilities} locale={locale} onSelect={setSelectedCapabilityId} scope="machine" selectedCapabilityId={selected.capability_id} t={t} />
305308

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { useCallback, useEffect, useState } from "react";
2+
import { Check, KeyRound, ShieldCheck, Trash2 } from "lucide-react";
3+
4+
import {
5+
fetchOperatorCredential,
6+
writeOperatorCredential,
7+
type OperatorCredential,
8+
} from "../../data/chat";
9+
import { type WorkspaceTranslate, useWorkspaceI18n } from "./i18n";
10+
11+
function localizeStatus(status: OperatorCredential["status"], t: WorkspaceTranslate) {
12+
if (status === "invalid") return t("machine.credentialInvalid");
13+
return t(status === "configured" ? "machine.credentialConfigured" : "machine.credentialAbsent");
14+
}
15+
16+
function localizeSource(source: string, t: WorkspaceTranslate) {
17+
if (source === "machine_store") return t("machine.credentialSourceMachine");
18+
if (source === "service_environment") return t("machine.credentialSourceEnvironment");
19+
return t("machine.credentialSourceUnset");
20+
}
21+
22+
/**
23+
* The one place a person stores the operator model credential.
24+
*
25+
* The key is write-only end to end: this form submits one, and every readback
26+
* it renders is the redacted projection, so the browser can configure a
27+
* credential it is never able to display again. The endpoint is not a secret
28+
* and reads back as itself.
29+
*/
30+
export function OperatorCredentialSettings() {
31+
const { t } = useWorkspaceI18n();
32+
const [credential, setCredential] = useState<OperatorCredential | null>(null);
33+
const [apiKey, setApiKey] = useState("");
34+
const [baseUrl, setBaseUrl] = useState("");
35+
const [busy, setBusy] = useState<"" | "load" | "store">("");
36+
const [error, setError] = useState<string | null>(null);
37+
const [notice, setNotice] = useState<string | null>(null);
38+
39+
const reload = useCallback(async () => {
40+
setBusy("load");
41+
try {
42+
const loaded = await fetchOperatorCredential();
43+
setCredential(loaded);
44+
// Only a non-secret field is prefilled; the key never round-trips.
45+
setBaseUrl(String(loaded.base_url.value ?? ""));
46+
} catch (cause) {
47+
setError(cause instanceof Error ? cause.message : t("machine.credentialError"));
48+
} finally {
49+
setBusy("");
50+
}
51+
}, [t]);
52+
53+
useEffect(() => {
54+
void reload();
55+
}, [reload]);
56+
57+
async function submit(update: Parameters<typeof writeOperatorCredential>[0], done: string) {
58+
setBusy("store");
59+
setError(null);
60+
setNotice(null);
61+
try {
62+
const stored = await writeOperatorCredential(update);
63+
setCredential(stored);
64+
setBaseUrl(String(stored.base_url.value ?? ""));
65+
setApiKey("");
66+
setNotice(done);
67+
} catch (cause) {
68+
setError(cause instanceof Error ? cause.message : t("machine.credentialError"));
69+
} finally {
70+
setBusy("");
71+
}
72+
}
73+
74+
if (!credential && busy === "load") {
75+
return <div className="personal-machine-loading" role="status">{t("common.loading")}</div>;
76+
}
77+
78+
const keyLabel = credential
79+
? `${localizeStatus(credential.provider_key.configured ? "configured" : "absent", t)} · ${localizeSource(credential.provider_key.source, t)}`
80+
: "";
81+
const urlLabel = credential
82+
? `${credential.base_url.value ?? t("machine.credentialAbsent")} · ${localizeSource(credential.base_url.source, t)}`
83+
: "";
84+
85+
return (
86+
<section className="personal-operator-credential" data-testid="operator-credential-settings">
87+
<header>
88+
<KeyRound aria-hidden size={17} />
89+
<div>
90+
<strong>{t("machine.credentialTitle")}</strong>
91+
<p>{t("machine.credentialDescription")}</p>
92+
</div>
93+
<span className="personal-operator-credential-status">
94+
{credential ? localizeStatus(credential.status, t) : t("common.loading")}
95+
</span>
96+
</header>
97+
98+
{credential ? (
99+
<dl className="personal-operator-credential-readback">
100+
<div>
101+
<dt>{t("machine.credentialApiKey")}</dt>
102+
<dd>{keyLabel}</dd>
103+
</div>
104+
<div>
105+
<dt>{t("machine.credentialFingerprint")}</dt>
106+
<dd><code>{credential.provider_key.fingerprint ?? t("common.none")}</code></dd>
107+
</div>
108+
<div>
109+
<dt>{t("machine.credentialBaseUrl")}</dt>
110+
<dd>{urlLabel}</dd>
111+
</div>
112+
</dl>
113+
) : null}
114+
115+
{credential?.status === "invalid" && credential.repair ? (
116+
<p className="personal-machine-error" role="alert">{credential.repair}</p>
117+
) : null}
118+
119+
<label htmlFor="operator-credential-api-key">
120+
<span>{t("machine.credentialApiKey")}</span>
121+
<input
122+
autoComplete="off"
123+
disabled={Boolean(busy)}
124+
id="operator-credential-api-key"
125+
onChange={(event) => setApiKey(event.target.value)}
126+
placeholder={t("machine.credentialApiKeyPlaceholder")}
127+
type="password"
128+
value={apiKey}
129+
/>
130+
</label>
131+
132+
<label htmlFor="operator-credential-base-url">
133+
<span>{t("machine.credentialBaseUrl")}</span>
134+
<input
135+
autoComplete="off"
136+
disabled={Boolean(busy)}
137+
id="operator-credential-base-url"
138+
onChange={(event) => setBaseUrl(event.target.value)}
139+
placeholder={t("machine.credentialBaseUrlPlaceholder")}
140+
type="text"
141+
value={baseUrl}
142+
/>
143+
</label>
144+
145+
{error ? <p className="personal-machine-error" role="alert">{error}</p> : null}
146+
{notice ? <p className="personal-machine-notice" role="status" aria-live="polite"><Check aria-hidden size={16} />{notice}</p> : null}
147+
148+
{/* Not `personal-capability-actions`: the browser smoke treats that class
149+
as the capability editor's own action row, and this panel renders on
150+
the same page. */}
151+
<footer className="personal-operator-credential-actions">
152+
<button
153+
className="is-primary"
154+
disabled={Boolean(busy) || (!apiKey.trim() && !baseUrl.trim())}
155+
onClick={() => void submit(
156+
{
157+
...(apiKey.trim() ? { provider_key: apiKey } : {}),
158+
...(baseUrl.trim() ? { base_url: baseUrl } : {}),
159+
},
160+
t("machine.credentialStored"),
161+
)}
162+
type="button"
163+
>
164+
{busy === "store" ? t("common.loading") : t("machine.credentialStore")}
165+
</button>
166+
<button
167+
disabled={Boolean(busy) || credential?.provider_key.configured !== true}
168+
onClick={() => void submit({ clear_provider_key: true }, t("machine.credentialCleared"))}
169+
type="button"
170+
>
171+
<Trash2 aria-hidden size={15} />{t("machine.credentialClearKey")}
172+
</button>
173+
<button
174+
disabled={Boolean(busy) || credential?.base_url.configured !== true}
175+
onClick={() => void submit({ clear_base_url: true }, t("machine.credentialCleared"))}
176+
type="button"
177+
>
178+
<Trash2 aria-hidden size={15} />{t("machine.credentialClearUrl")}
179+
</button>
180+
</footer>
181+
182+
<details className="personal-capability-scope-note">
183+
<summary><ShieldCheck aria-hidden size={17} />{t("machine.credentialTitle")}</summary>
184+
<p>{t("machine.credentialBoundary")}</p>
185+
</details>
186+
</section>
187+
);
188+
}

0 commit comments

Comments
 (0)