fix: use Pi user auth and handle startup failures
This commit is contained in:
@@ -12,6 +12,7 @@ MAX_PI_PROCESSES=4
|
|||||||
PI_PROVIDER=
|
PI_PROVIDER=
|
||||||
PI_MODEL=
|
PI_MODEL=
|
||||||
PI_THINKING=
|
PI_THINKING=
|
||||||
|
PI_AUTH_FILE=${HOME}/.pi/agent/auth.json
|
||||||
|
|
||||||
# Set these for the selected DWH/vector/embedding adapters.
|
# Set these for the selected DWH/vector/embedding adapters.
|
||||||
THT_DB_NAME=
|
THT_DB_NAME=
|
||||||
|
|||||||
+23
-1
@@ -1,8 +1,30 @@
|
|||||||
# ThothII — Project State
|
# ThothII — Project State
|
||||||
|
|
||||||
> Starting-point snapshot for new sessions. Last updated: 2026-07-15 (central live log and compact CTE density live).
|
> Starting-point snapshot for new sessions. Last updated: 2026-07-21 (local Pi user-auth wiring and startup error handling live).
|
||||||
> Point a fresh session here ("read PROJECT_STATE.md") before substantial work.
|
> Point a fresh session here ("read PROJECT_STATE.md") before substantial work.
|
||||||
|
|
||||||
|
## Local Pi user auth + startup failure handling — LIVE 2026-07-21
|
||||||
|
|
||||||
|
- The PSD Docker profile now bind-mounts the configurable host `PI_AUTH_FILE` read-only at
|
||||||
|
`/home/thoth/.pi/agent/auth.json`; on this Mac it resolves to the real user profile
|
||||||
|
`/Users/mp/.pi/agent/auth.json`. The container keeps its correct Linux identity
|
||||||
|
`HOME=/home/thoth` while Pi sees the user's independent `deepseek` and `zai` credentials.
|
||||||
|
- `deploy/pi/settings.json` is the non-secret model policy and exposes, in order,
|
||||||
|
`zai/glm-5.2`, `deepseek/deepseek-v4-flash`, `deepseek/deepseek-v4-pro`, and
|
||||||
|
`aritmolab/qwen3.6-35b-a3b`. The core image is aligned to Pi 0.80.3.
|
||||||
|
- New-session creation now validates the saved provider/model against Pi before persistence;
|
||||||
|
unavailable selections return sanitized `503 model_unavailable` without creating a manifest.
|
||||||
|
A synchronous runtime-construction failure after persistence marks that session `failed` and
|
||||||
|
returns the fixed startup-recovery message instead of leaving an ambiguous `open` session.
|
||||||
|
- Verification: backend 235/235, TypeScript clean, dedicated Compose auth/model contract green
|
||||||
|
with a demonstrated RED→GREEN cycle. Rebuilt core image
|
||||||
|
`sha256:a8b4dd9f016c2335e4da897073bc6d5bdf1e8ce9b60dcfcca171b6677f563228`
|
||||||
|
is healthy; live `/models` returned all four models; a real `deepseek-v4-pro` smoke reached its
|
||||||
|
first reviewer gate, deleted only its own session, and restored the exact prior settings.
|
||||||
|
- Deleted the three explicitly approved incomplete DeepSeek attempts:
|
||||||
|
`a390c8b8-0a91-4a37-967b-ce7ff9be9797`, `a2f974b2-4c48-4967-b4b6-afdbc2b2d541`, and
|
||||||
|
`f66e1959-3c71-4b10-8aa1-606992046b7e` (API delete 204, subsequent lookup 404 for each).
|
||||||
|
|
||||||
## User-owned sessions cutover — prepared, manual gate pending (2026-07-16)
|
## User-owned sessions cutover — prepared, manual gate pending (2026-07-16)
|
||||||
|
|
||||||
- **Target contract:** the public server runs `AUTH_MODE=upstream` with Task 4 portal identity
|
- **Target contract:** the public server runs `AUTH_MODE=upstream` with Task 4 portal identity
|
||||||
|
|||||||
+2
-1
@@ -95,7 +95,8 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
|
|||||||
app.get("/health/dwh", async () => tht.dbPing());
|
app.get("/health/dwh", async () => tht.dbPing());
|
||||||
app.get("/me", async (req) => getPrincipal(req));
|
app.get("/me", async (req) => getPrincipal(req));
|
||||||
sessionRoutes(app, {
|
sessionRoutes(app, {
|
||||||
mgr, tht: tht as ThtRunner, hub, getSettings, readiness, dwhPrecheck: config.dwhPrecheck,
|
mgr, tht: tht as ThtRunner, hub, getSettings, readiness, listModels,
|
||||||
|
dwhPrecheck: config.dwhPrecheck,
|
||||||
});
|
});
|
||||||
sqlRoutes(app, { tht: tht as ThtRunner, getSettings });
|
sqlRoutes(app, { tht: tht as ThtRunner, getSettings });
|
||||||
metaRoutes(app, { harnessDir: config.harnessDir, listModels });
|
metaRoutes(app, { harnessDir: config.harnessDir, listModels });
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { Settings } from "../settings/settings-store.js";
|
|||||||
import { getPrincipal } from "../auth/auth.js";
|
import { getPrincipal } from "../auth/auth.js";
|
||||||
import type { PrincipalContext } from "../auth/principal.js";
|
import type { PrincipalContext } from "../auth/principal.js";
|
||||||
import type { ReadinessManager } from "../runtime/readiness-manager.js";
|
import type { ReadinessManager } from "../runtime/readiness-manager.js";
|
||||||
|
import type { ListModelsFn } from "./meta.js";
|
||||||
|
|
||||||
const BOOTSTRAP_FAILURE_MESSAGE =
|
const BOOTSTRAP_FAILURE_MESSAGE =
|
||||||
"Session startup failed. Check configuration and connectivity, then Resume the session.";
|
"Session startup failed. Check configuration and connectivity, then Resume the session.";
|
||||||
@@ -15,6 +16,8 @@ const RESUME_FAILURE_MESSAGE =
|
|||||||
"Session could not be resumed. Check configuration and connectivity, then try again.";
|
"Session could not be resumed. Check configuration and connectivity, then try again.";
|
||||||
const DWH_UNREACHABLE_MESSAGE =
|
const DWH_UNREACHABLE_MESSAGE =
|
||||||
"Cannot start a session: the database is unreachable. Check the VPN connection and try again.";
|
"Cannot start a session: the database is unreachable. Check the VPN connection and try again.";
|
||||||
|
const MODEL_UNAVAILABLE_MESSAGE =
|
||||||
|
"Selected model is unavailable. Check Pi authentication and model settings, then try again.";
|
||||||
|
|
||||||
export function sessionRoutes(
|
export function sessionRoutes(
|
||||||
app: FastifyInstance,
|
app: FastifyInstance,
|
||||||
@@ -22,6 +25,7 @@ export function sessionRoutes(
|
|||||||
mgr: PiProcessManager; tht: ThtRunner; hub: SseHub;
|
mgr: PiProcessManager; tht: ThtRunner; hub: SseHub;
|
||||||
getSettings: (principal: PrincipalContext) => Promise<Settings>;
|
getSettings: (principal: PrincipalContext) => Promise<Settings>;
|
||||||
readiness: ReadinessManager;
|
readiness: ReadinessManager;
|
||||||
|
listModels: ListModelsFn;
|
||||||
/** Local-only guard: probe DWH reachability before creating a session (run-stack.sh). */
|
/** Local-only guard: probe DWH reachability before creating a session (run-stack.sh). */
|
||||||
dwhPrecheck?: boolean;
|
dwhPrecheck?: boolean;
|
||||||
},
|
},
|
||||||
@@ -189,6 +193,26 @@ export function sessionRoutes(
|
|||||||
return reply.code(503).send({ error: DWH_UNREACHABLE_MESSAGE, code: "dwh_unreachable" });
|
return reply.code(503).send({ error: DWH_UNREACHABLE_MESSAGE, code: "dwh_unreachable" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (s.provider && s.model) {
|
||||||
|
let available: Awaited<ReturnType<ListModelsFn>>;
|
||||||
|
try {
|
||||||
|
available = await d.listModels();
|
||||||
|
} catch {
|
||||||
|
return reply.code(503).send({
|
||||||
|
error: MODEL_UNAVAILABLE_MESSAGE,
|
||||||
|
code: "model_unavailable",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const selectedAvailable = available.some(
|
||||||
|
(candidate) => candidate.provider === s.provider && candidate.id === s.model,
|
||||||
|
);
|
||||||
|
if (!selectedAvailable) {
|
||||||
|
return reply.code(503).send({
|
||||||
|
error: MODEL_UNAVAILABLE_MESSAGE,
|
||||||
|
code: "model_unavailable",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
// Settings (global) supply workspace/provider/model/thinking. The new-question
|
// Settings (global) supply workspace/provider/model/thinking. The new-question
|
||||||
// form sends only the question text. `workspace` selects the tht `-c <config>`.
|
// form sends only the question text. `workspace` selects the tht `-c <config>`.
|
||||||
let id: string;
|
let id: string;
|
||||||
@@ -206,8 +230,21 @@ export function sessionRoutes(
|
|||||||
principal,
|
principal,
|
||||||
question: b.question,
|
question: b.question,
|
||||||
};
|
};
|
||||||
const rt = d.mgr.createFor(id, options);
|
let rt: ReturnType<PiProcessManager["createFor"]> | undefined;
|
||||||
bindRuntime(id, rt, runner, s.workspace);
|
try {
|
||||||
|
rt = d.mgr.createFor(id, options);
|
||||||
|
bindRuntime(id, rt, runner, s.workspace);
|
||||||
|
} catch (error) {
|
||||||
|
if (rt) d.mgr.teardownIfCurrent(id, rt);
|
||||||
|
console.error(
|
||||||
|
`[pi:${id}] runtime construction failed:`,
|
||||||
|
error instanceof Error ? error.message : "unknown error",
|
||||||
|
);
|
||||||
|
await runner.failSession(id, s.workspace).catch((persistenceError: unknown) => {
|
||||||
|
console.error(`[session:${id}] failSession persistence failed:`, persistenceError);
|
||||||
|
});
|
||||||
|
return reply.code(503).send({ error: BOOTSTRAP_FAILURE_MESSAGE });
|
||||||
|
}
|
||||||
info(id, "Session created");
|
info(id, "Session created");
|
||||||
bootstrap(
|
bootstrap(
|
||||||
id, rt, runner, s.workspace, d.mgr.configure(rt, options),
|
id, rt, runner, s.workspace, d.mgr.configure(rt, options),
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ test("POST /sessions usa i settings (workspace/provider/model/thinking) e crea+a
|
|||||||
sessionList: async () => [{ id: "s1" }],
|
sessionList: async () => [{ id: "s1" }],
|
||||||
} as any,
|
} as any,
|
||||||
getSettings: () => ({ workspace: "w", provider: "zai", model: "glm-5.2", thinking: "high" }),
|
getSettings: () => ({ workspace: "w", provider: "zai", model: "glm-5.2", thinking: "high" }),
|
||||||
|
listModels: async () => [
|
||||||
|
{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true },
|
||||||
|
],
|
||||||
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
||||||
});
|
});
|
||||||
const created = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
const created = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
||||||
@@ -259,6 +262,9 @@ test("POST /sessions configura Pi con il thinking globale selezionato", async ()
|
|||||||
sessionNew: async () => ({ id: "s-thinking" }),
|
sessionNew: async () => ({ id: "s-thinking" }),
|
||||||
} as any,
|
} as any,
|
||||||
getSettings: () => ({ workspace: "psd", provider: "zai", model: "glm-5.2", thinking: "high" }) as any,
|
getSettings: () => ({ workspace: "psd", provider: "zai", model: "glm-5.2", thinking: "high" }) as any,
|
||||||
|
listModels: async () => [
|
||||||
|
{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
||||||
@@ -1539,6 +1545,69 @@ test("POST /sessions proceeds when ollamaEnsure succeeds", async () => {
|
|||||||
expect(ensureWs).toBe("psd");
|
expect(ensureWs).toBe("psd");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /sessions rejects an unavailable saved model before persisting a session", async () => {
|
||||||
|
let created = 0;
|
||||||
|
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||||
|
thtRunner: {
|
||||||
|
sessionNew: async () => { created += 1; return { id: "must-not-exist" }; },
|
||||||
|
} as any,
|
||||||
|
readiness: { ensure: async () => ({ ok: true }) } as any,
|
||||||
|
getSettings: () => ({
|
||||||
|
workspace: "psd",
|
||||||
|
provider: "deepseek",
|
||||||
|
model: "deepseek-v4-pro",
|
||||||
|
thinking: "medium",
|
||||||
|
}) as any,
|
||||||
|
listModels: async () => [
|
||||||
|
{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(503);
|
||||||
|
expect(res.json()).toEqual({
|
||||||
|
error: "Selected model is unavailable. Check Pi authentication and model settings, then try again.",
|
||||||
|
code: "model_unavailable",
|
||||||
|
});
|
||||||
|
expect(created).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("POST /sessions marks a persisted session failed when runtime construction throws", async () => {
|
||||||
|
let failed = 0;
|
||||||
|
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||||
|
mgr: {
|
||||||
|
createFor: () => { throw new Error("provider bootstrap unavailable"); },
|
||||||
|
} as any,
|
||||||
|
thtRunner: {
|
||||||
|
sessionNew: async () => ({ id: "s-runtime-failure" }),
|
||||||
|
failSession: async (id: string, workspace: string) => {
|
||||||
|
expect(id).toBe("s-runtime-failure");
|
||||||
|
expect(workspace).toBe("psd");
|
||||||
|
failed += 1;
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
readiness: { ensure: async () => ({ ok: true }) } as any,
|
||||||
|
getSettings: () => ({
|
||||||
|
workspace: "psd",
|
||||||
|
provider: "deepseek",
|
||||||
|
model: "deepseek-v4-pro",
|
||||||
|
thinking: "medium",
|
||||||
|
}) as any,
|
||||||
|
listModels: async () => [
|
||||||
|
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(503);
|
||||||
|
expect(res.json()).toEqual({
|
||||||
|
error: "Session startup failed. Check configuration and connectivity, then Resume the session.",
|
||||||
|
});
|
||||||
|
expect(failed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test("POST /sessions/:id/resume returns 409 for a read-only session without calling ollamaEnsure", async () => {
|
test("POST /sessions/:id/resume returns 409 for a read-only session without calling ollamaEnsure", async () => {
|
||||||
let ensureCalled = false;
|
let ensureCalled = false;
|
||||||
const app = mutApp({
|
const app = mutApp({
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /home/chirone/thothii-data:/data
|
- /home/chirone/thothii-data:/data
|
||||||
- /home/chirone/thothii-data/pi-config:/home/thoth/.pi
|
- /home/chirone/thothii-data/pi-config:/home/thoth/.pi
|
||||||
|
- ${PI_AUTH_FILE:?set PI_AUTH_FILE}:/home/thoth/.pi/agent/auth.json:ro
|
||||||
- /home/chirone/chirone/etl/docs/evidence:/data/evidence:ro
|
- /home/chirone/chirone/etl/docs/evidence:/data/evidence:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
{
|
{
|
||||||
"defaultProjectTrust": "always"
|
"defaultProjectTrust": "always",
|
||||||
|
"enabledModels": [
|
||||||
|
"zai/glm-5.2",
|
||||||
|
"deepseek/deepseek-v4-flash",
|
||||||
|
"deepseek/deepseek-v4-pro",
|
||||||
|
"aritmolab/qwen3.6-35b-a3b"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.7
|
||||||
# thothii-core: Fastify (Node 22) + harness Python 3.12 (tht CLI) + runtime Pi.
|
# thothii-core: Fastify (Node 22) + harness Python 3.12 (tht CLI) + runtime Pi.
|
||||||
# Singolo container, entrypoint logico "server" (default).
|
# Singolo container, entrypoint logico "server" (default).
|
||||||
ARG PI_VERSION=0.80.2
|
ARG PI_VERSION=0.80.3
|
||||||
|
|
||||||
# ---- Stage 1: backend TypeScript -> dist ----
|
# ---- Stage 1: backend TypeScript -> dist ----
|
||||||
FROM node:22-bookworm AS backend-build
|
FROM node:22-bookworm AS backend-build
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Pi User Authentication and Startup Error Handling Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make Dockerized Pi consume the configured user auth store, expose four approved models, fail session startup cleanly, and remove three incomplete attempts.
|
||||||
|
|
||||||
|
**Architecture:** Compose mounts only the host Pi `auth.json` into the container while repository-owned Pi settings define the model policy. The backend validates the persisted model before creating a session and converts post-persistence runtime-construction errors into a failed session plus a sanitized 503.
|
||||||
|
|
||||||
|
**Tech Stack:** Docker Compose, Pi 0.80.3, Fastify/TypeScript, Vitest, shell deployment-contract tests.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Never copy or log provider keys.
|
||||||
|
- Mount the auth file read-only.
|
||||||
|
- Enabled model order is ZAI, DeepSeek Flash, DeepSeek Pro, local Qwen.
|
||||||
|
- UI error strings remain English.
|
||||||
|
- Preserve the question in the frontend retry composer.
|
||||||
|
- Delete only the three explicitly approved incomplete session IDs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Container Pi auth and model policy
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `compose.yaml`
|
||||||
|
- Modify: `.env.example`
|
||||||
|
- Modify: `deploy/pi/settings.json`
|
||||||
|
- Modify: `docker/core.Dockerfile`
|
||||||
|
- Test: `scripts/test-default-compose.sh`
|
||||||
|
- Test: `scripts/test-container-deployment.sh`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `PI_AUTH_FILE`, an absolute readable host path.
|
||||||
|
- Produces: `/home/thoth/.pi/agent/auth.json` read-only and a four-entry `enabledModels` policy.
|
||||||
|
|
||||||
|
- [ ] Add failing deployment-contract assertions for the auth mount, four enabled models, and Pi 0.80.3.
|
||||||
|
- [ ] Run the focused shell tests and confirm the expected failures.
|
||||||
|
- [ ] Add the configurable auth mount, model policy, environment documentation, and Pi version alignment.
|
||||||
|
- [ ] Re-run the focused shell tests and confirm they pass.
|
||||||
|
|
||||||
|
### Task 2: Pre-persistence model availability guard
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/src/app.ts`
|
||||||
|
- Modify: `backend/src/routes/sessions.ts`
|
||||||
|
- Test: `backend/test/routes-sessions.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the existing `ListModelsFn` used by model/settings routes.
|
||||||
|
- Produces: a sanitized 503 before `sessionNew` when saved settings are unavailable.
|
||||||
|
|
||||||
|
- [ ] Add a failing route test proving an unavailable model returns 503 and never calls `sessionNew`.
|
||||||
|
- [ ] Run the single Vitest test and confirm the expected failure.
|
||||||
|
- [ ] Inject the model-list dependency into session routes and implement the minimal guard.
|
||||||
|
- [ ] Re-run the focused test and confirm it passes.
|
||||||
|
|
||||||
|
### Task 3: Post-persistence runtime-construction failure
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/src/routes/sessions.ts`
|
||||||
|
- Test: `backend/test/routes-sessions.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `ThtRunner.failSession(id, workspace)` and `PiProcessManager.createFor`.
|
||||||
|
- Produces: failed persisted state plus sanitized 503 when runtime construction throws.
|
||||||
|
|
||||||
|
- [ ] Add a failing route test for a throwing `createFor` after `sessionNew`.
|
||||||
|
- [ ] Run the single test and confirm it fails because the route currently returns 500 and leaves the session open.
|
||||||
|
- [ ] Catch runtime construction/binding failures, persist `failed`, and return the startup error.
|
||||||
|
- [ ] Re-run the focused test and confirm it passes.
|
||||||
|
|
||||||
|
### Task 4: Cleanup and full verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: external PSD session store only through `tht session delete`.
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the three approved session IDs.
|
||||||
|
- Produces: no corresponding directories or API rows.
|
||||||
|
|
||||||
|
- [ ] Run backend Vitest and TypeScript typecheck.
|
||||||
|
- [ ] Run deployment-contract tests and render Compose with the real auth path.
|
||||||
|
- [ ] Rebuild/recreate the core container and verify health.
|
||||||
|
- [ ] Verify `/models` returns all four enabled models in order.
|
||||||
|
- [ ] Start a uniquely named DeepSeek smoke and observe the first reviewer gate; clean up only the smoke.
|
||||||
|
- [ ] Delete the three approved incomplete sessions and verify they are absent.
|
||||||
|
- [ ] Run `git diff --check` and inspect the final diff for secret leakage.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Pi User Authentication and Startup Error Handling
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make the Dockerized ThothII runtime use the same provider authentication that Pi stores for
|
||||||
|
the host user, expose only the explicitly enabled model set (including DeepSeek V4 Pro), and
|
||||||
|
avoid leaving apparently healthy `open` sessions when model startup cannot begin.
|
||||||
|
|
||||||
|
## Runtime configuration
|
||||||
|
|
||||||
|
- Keep the container user `thoth` and `HOME=/home/thoth`; those are valid container-local
|
||||||
|
identities and must not be changed to a macOS path.
|
||||||
|
- Bind-mount a configurable host Pi auth file, `PI_AUTH_FILE`, to
|
||||||
|
`/home/thoth/.pi/agent/auth.json` read-only. The default local value is
|
||||||
|
`${HOME}/.pi/agent/auth.json`; deployments may override it with another absolute path.
|
||||||
|
- Keep `deploy/pi/settings.json` as the non-secret runtime policy. It must enable, in order:
|
||||||
|
`zai/glm-5.2`, `deepseek/deepseek-v4-flash`, `deepseek/deepseek-v4-pro`, and
|
||||||
|
`aritmolab/qwen3.6-35b-a3b`.
|
||||||
|
- Keep `deploy/pi/models.json` for custom provider definitions only. Provider credentials stay
|
||||||
|
exclusively in Pi's user auth file and are never copied into the repository.
|
||||||
|
- Run Pi 0.80.3 in the core image, matching the audited backend provider contract.
|
||||||
|
|
||||||
|
## Session-start behavior
|
||||||
|
|
||||||
|
The settings write endpoint remains the main model-validation boundary. Session creation adds a
|
||||||
|
defense-in-depth availability check before persistence: if the saved provider/model is absent
|
||||||
|
from Pi's current enabled and authenticated model list, return a sanitized 503 and do not call
|
||||||
|
`tht session new`.
|
||||||
|
|
||||||
|
If runtime construction fails after persistence despite that check (for example a race or local
|
||||||
|
process limit), catch the error, mark the just-created session failed, and return the same
|
||||||
|
sanitized startup error. Never expose provider credentials or raw Pi errors to the browser.
|
||||||
|
Asynchronous bootstrap failures continue to emit `session_failed` and persist failed state.
|
||||||
|
|
||||||
|
## Cleanup and verification
|
||||||
|
|
||||||
|
Delete only these incomplete sessions:
|
||||||
|
|
||||||
|
- `a390c8b8-0a91-4a37-967b-ce7ff9be9797`
|
||||||
|
- `a2f974b2-4c48-4967-b4b6-afdbc2b2d541`
|
||||||
|
- `f66e1959-3c71-4b10-8aa1-606992046b7e`
|
||||||
|
|
||||||
|
Verify configuration contracts, backend tests and typecheck, rendered Compose mounts, `/models`
|
||||||
|
containing all four configured models, and a live DeepSeek startup reaching its first reviewer
|
||||||
|
gate. The auth file must remain read-only and no secret value may appear in rendered Compose,
|
||||||
|
logs, tests, or source control.
|
||||||
@@ -39,6 +39,7 @@ MAX_PI_PROCESSES=4
|
|||||||
PI_PROVIDER=zai
|
PI_PROVIDER=zai
|
||||||
PI_MODEL=glm-5.2
|
PI_MODEL=glm-5.2
|
||||||
PI_THINKING=medium
|
PI_THINKING=medium
|
||||||
|
PI_AUTH_FILE=$auth_file
|
||||||
THT_PROFILE=workstation
|
THT_PROFILE=workstation
|
||||||
THT_DB_NAME=$(required THT_DB_NAME)
|
THT_DB_NAME=$(required THT_DB_NAME)
|
||||||
THT_DWH_REST_URL=$(required THT_DWH_REST_URL)
|
THT_DWH_REST_URL=$(required THT_DWH_REST_URL)
|
||||||
|
|||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||||
|
auth_file="$tmp/auth.json"
|
||||||
|
printf '%s\n' '{}' >"$auth_file"
|
||||||
|
chmod 0600 "$auth_file"
|
||||||
|
|
||||||
|
rendered=$(PI_AUTH_FILE="$auth_file" docker compose config)
|
||||||
|
printf '%s\n' "$rendered" | grep -q "source: $auth_file"
|
||||||
|
printf '%s\n' "$rendered" | grep -q 'target: /home/thoth/.pi/agent/auth.json'
|
||||||
|
printf '%s\n' "$rendered" | grep -A4 'target: /home/thoth/.pi/agent/auth.json' \
|
||||||
|
| grep -q 'read_only: true'
|
||||||
|
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
settings = json.loads(Path("deploy/pi/settings.json").read_text())
|
||||||
|
assert settings["enabledModels"] == [
|
||||||
|
"zai/glm-5.2",
|
||||||
|
"deepseek/deepseek-v4-flash",
|
||||||
|
"deepseek/deepseek-v4-pro",
|
||||||
|
"aritmolab/qwen3.6-35b-a3b",
|
||||||
|
]
|
||||||
|
PY
|
||||||
|
|
||||||
|
grep -q '^ARG PI_VERSION=0.80.3$' docker/core.Dockerfile
|
||||||
|
grep -q '^PI_AUTH_FILE=$auth_file$' scripts/bootstrap-local-psd-docker-config.sh
|
||||||
|
|
||||||
|
echo "Pi user-auth Compose contract passed."
|
||||||
Reference in New Issue
Block a user