fix(docker): run real questions through trusted Pi gate
This commit is contained in:
@@ -17,8 +17,10 @@ PI_THINKING=
|
|||||||
THT_DB_NAME=
|
THT_DB_NAME=
|
||||||
THT_DWH_REST_URL=
|
THT_DWH_REST_URL=
|
||||||
THT_VEC_REST_URL=
|
THT_VEC_REST_URL=
|
||||||
|
THT_VEC_WRITE_REST_URL=
|
||||||
THT_OLLAMA_URL=
|
THT_OLLAMA_URL=
|
||||||
THT_DOCS_ROOT=/data/workspaces/example/evidence-source
|
THT_DOCS_ROOT=/data/workspaces/example/evidence-source
|
||||||
|
THT_PROFILE=server
|
||||||
|
|
||||||
# Local-vector defaults (used by the optional local-vector overlay).
|
# Local-vector defaults (used by the optional local-vector overlay).
|
||||||
THT_VECTOR_DATABASE=thoth
|
THT_VECTOR_DATABASE=thoth
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ config/ca-chain.pem
|
|||||||
|
|
||||||
# ThothII deployment configuration and secret values (keep only the README tracked)
|
# ThothII deployment configuration and secret values (keep only the README tracked)
|
||||||
deploy/.env
|
deploy/.env
|
||||||
|
deploy/compose.psd-local.yaml
|
||||||
|
deploy/workspaces/psd.yaml
|
||||||
deploy/secrets/*
|
deploy/secrets/*
|
||||||
!deploy/secrets/README.md
|
!deploy/secrets/README.md
|
||||||
!deploy/secrets/*.example
|
!deploy/secrets/*.example
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ function unavailable(): Error { return new Error("secret bundle is unavailable")
|
|||||||
function secureStat(info: Stats, docker: boolean): boolean {
|
function secureStat(info: Stats, docker: boolean): boolean {
|
||||||
const mode = info.mode & 0o777;
|
const mode = info.mode & 0o777;
|
||||||
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > MAX_BUNDLE_BYTES) return false;
|
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > MAX_BUNDLE_BYTES) return false;
|
||||||
if (docker) return info.uid === 0 && mode === 0o444;
|
if (docker) {
|
||||||
|
return (info.uid === 0 && mode === 0o444)
|
||||||
|
|| (info.uid === (process.getuid?.() ?? info.uid) && (mode === 0o400 || mode === 0o600));
|
||||||
|
}
|
||||||
return info.uid === (process.getuid?.() ?? info.uid) && (mode === 0o400 || mode === 0o600);
|
return info.uid === (process.getuid?.() ?? info.uid) && (mode === 0o400 || mode === 0o600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ export class PiProcessManager {
|
|||||||
credentialFile: this.cfg.modelApiKeyFile,
|
credentialFile: this.cfg.modelApiKeyFile,
|
||||||
additions: { THT_SESSION: sessionId, THT_AUTHOR: author },
|
additions: { THT_SESSION: sessionId, THT_AUTHOR: author },
|
||||||
});
|
});
|
||||||
|
// The Thoth gate executes the deterministic `tht` CLI as a Pi tool. Give only
|
||||||
|
// this managed session process the adapter values already loaded by the core
|
||||||
|
// entrypoint; the generic provider helper continues to scrub them by default.
|
||||||
|
for (const name of [
|
||||||
|
"THT_DWH_API_KEY", "THT_VEC_API_KEY", "THT_VEC_WRITE_API_KEY", "THT_SSL_CA",
|
||||||
|
] as const) {
|
||||||
|
if (process.env[name] !== undefined) env[name] = process.env[name];
|
||||||
|
}
|
||||||
delete env.THT_DATA_ROOT;
|
delete env.THT_DATA_ROOT;
|
||||||
if (this.cfg.dataRoot !== undefined) env.THT_DATA_ROOT = this.cfg.dataRoot;
|
if (this.cfg.dataRoot !== undefined) env.THT_DATA_ROOT = this.cfg.dataRoot;
|
||||||
// pi 0.73 removed `--approve`: rpc mode is headless and its argv is intentionally minimal.
|
// pi 0.73 removed `--approve`: rpc mode is headless and its argv is intentionally minimal.
|
||||||
@@ -62,7 +70,7 @@ export class PiProcessManager {
|
|||||||
|
|
||||||
async spawnFor(
|
async spawnFor(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
o: { provider?: string; model?: string; thinking?: string; author?: string; mode?: "new" | "resume" },
|
o: { provider?: string; model?: string; thinking?: string; author?: string; question?: string; mode?: "new" | "resume" },
|
||||||
): Promise<SessionRuntime> {
|
): Promise<SessionRuntime> {
|
||||||
// Idempotent per session id: tear down any existing runtime for this id
|
// Idempotent per session id: tear down any existing runtime for this id
|
||||||
// first (before the cap check) so a resume/respawn neither leaks the old
|
// first (before the cap check) so a resume/respawn neither leaks the old
|
||||||
@@ -110,7 +118,7 @@ export class PiProcessManager {
|
|||||||
|
|
||||||
const message = o.mode === "resume"
|
const message = o.mode === "resume"
|
||||||
? `/riprendi-sessione ${sessionId}`
|
? `/riprendi-sessione ${sessionId}`
|
||||||
: `/nuova-domanda "kickoff"`;
|
: `/nuova-domanda ${JSON.stringify(o.question ?? "")}`;
|
||||||
rpc.send({ type: "prompt", message });
|
rpc.send({ type: "prompt", message });
|
||||||
return rt;
|
return rt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ export function sessionRoutes(
|
|||||||
model: s.model,
|
model: s.model,
|
||||||
thinking: s.thinking,
|
thinking: s.thinking,
|
||||||
author: getUser(req).id,
|
author: getUser(req).id,
|
||||||
|
question: b.question,
|
||||||
});
|
});
|
||||||
rt.bridge.onClientEvent((e) => d.hub.publish(id, e.type, e));
|
rt.bridge.onClientEvent((e) => d.hub.publish(id, e.type, e));
|
||||||
return { id };
|
return { id };
|
||||||
});
|
});
|
||||||
app.get("/sessions", async () => d.tht.sessionList());
|
app.get("/sessions", async () => d.tht.sessionList(d.getSettings().workspace));
|
||||||
app.get("/sessions/:id", async (req) => d.tht.sessionShow((req.params as any).id));
|
app.get("/sessions/:id", async (req) => d.tht.sessionShow((req.params as any).id, d.getSettings().workspace));
|
||||||
app.post("/sessions/:id/response", async (req, reply) => {
|
app.post("/sessions/:id/response", async (req, reply) => {
|
||||||
const id = (req.params as any).id;
|
const id = (req.params as any).id;
|
||||||
const rt = d.mgr.get(id);
|
const rt = d.mgr.get(id);
|
||||||
@@ -50,7 +51,7 @@ export function sessionRoutes(
|
|||||||
});
|
});
|
||||||
app.post("/sessions/:id/resume", async (req, reply) => {
|
app.post("/sessions/:id/resume", async (req, reply) => {
|
||||||
const id = (req.params as any).id;
|
const id = (req.params as any).id;
|
||||||
const manifest = (await d.tht.sessionShow(id)) as { status?: string; archived?: boolean } | null;
|
const manifest = (await d.tht.sessionShow(id, d.getSettings().workspace)) as { status?: string; archived?: boolean } | null;
|
||||||
if (manifest?.status === "finalized" || manifest?.archived) {
|
if (manifest?.status === "finalized" || manifest?.archived) {
|
||||||
return reply.code(409).send({ error: "sessione in sola lettura (finalizzata o archiviata)" });
|
return reply.code(409).send({ error: "sessione in sola lettura (finalizzata o archiviata)" });
|
||||||
}
|
}
|
||||||
@@ -103,7 +104,7 @@ export function sessionRoutes(
|
|||||||
app.delete("/sessions/:id", async (req, reply) => {
|
app.delete("/sessions/:id", async (req, reply) => {
|
||||||
const id = (req.params as any).id;
|
const id = (req.params as any).id;
|
||||||
d.mgr.teardown(id); // drop any live runtime before deleting on disk
|
d.mgr.teardown(id); // drop any live runtime before deleting on disk
|
||||||
await d.tht.deleteSession(id);
|
await d.tht.deleteSession(id, d.getSettings().workspace);
|
||||||
return reply.code(204).send();
|
return reply.code(204).send();
|
||||||
});
|
});
|
||||||
app.get("/sessions/:id/documents", async (req) => d.tht.documents((req.params as any).id));
|
app.get("/sessions/:id/documents", async (req) => d.tht.documents((req.params as any).id));
|
||||||
|
|||||||
@@ -109,12 +109,12 @@ export class ThtRunner {
|
|||||||
return this.json<{ id: string }>(a, o.workspace);
|
return this.json<{ id: string }>(a, o.workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionList() {
|
sessionList(workspace?: string) {
|
||||||
return this.json<SessionRow[]>(["session", "list", "--json"]);
|
return this.json<SessionRow[]>(["session", "list", "--json"], workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionShow(id: string) {
|
sessionShow(id: string, workspace?: string) {
|
||||||
return this.json<unknown>(["session", "show", id, "--json"]);
|
return this.json<unknown>(["session", "show", id, "--json"], workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlPreview(id: string, p: { limit?: number; offset?: number }) {
|
sqlPreview(id: string, p: { limit?: number; offset?: number }) {
|
||||||
@@ -141,7 +141,10 @@ export class ThtRunner {
|
|||||||
setGroup(id: string, group: string) { return this.ok(["session", "set-group", id, "--group", group]); }
|
setGroup(id: string, group: string) { return this.ok(["session", "set-group", id, "--group", group]); }
|
||||||
archive(id: string) { return this.ok(["session", "archive", id]); }
|
archive(id: string) { return this.ok(["session", "archive", id]); }
|
||||||
unarchive(id: string) { return this.ok(["session", "unarchive", id]); }
|
unarchive(id: string) { return this.ok(["session", "unarchive", id]); }
|
||||||
deleteSession(id: string) { return this.ok(["session", "delete", id]); }
|
async deleteSession(id: string, workspace?: string) {
|
||||||
|
const { code, stderr } = await this.run(["session", "delete", id], workspace);
|
||||||
|
if (code !== 0) throw new Error(`tht session delete exit ${code}: ${stderr.trim()}`);
|
||||||
|
}
|
||||||
documents(id: string) { return this.json<SessionDocument[]>(["session", "documents", id, "--json"]); }
|
documents(id: string) { return this.json<SessionDocument[]>(["session", "documents", id, "--json"]); }
|
||||||
|
|
||||||
async ollamaEnsure(workspace: string, timeoutSec: number): Promise<OllamaEnsureResult> {
|
async ollamaEnsure(workspace: string, timeoutSec: number): Promise<OllamaEnsureResult> {
|
||||||
|
|||||||
@@ -127,6 +127,17 @@ test("spawnFor default (new) mode sends /nuova-domanda", async () => {
|
|||||||
mgr.teardown("sid-10");
|
mgr.teardown("sid-10");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("spawnFor new mode forwards the real question instead of kickoff", async () => {
|
||||||
|
const cfg = loadConfig({});
|
||||||
|
const child = recordingChild();
|
||||||
|
const mgr = new PiProcessManager(cfg, { spawnFn: () => child as any });
|
||||||
|
await mgr.spawnFor("sid-question", { question: "pazienti con cardioversione e ILR" });
|
||||||
|
const prompt = JSON.parse(child._writes.at(-1)!);
|
||||||
|
expect(prompt.message).toBe('/nuova-domanda "pazienti con cardioversione e ILR"');
|
||||||
|
expect(prompt.message).not.toContain("kickoff");
|
||||||
|
mgr.teardown("sid-question");
|
||||||
|
});
|
||||||
|
|
||||||
test("production spawn uses explicit Pi path and passes portable data root without rewriting PATH", async () => {
|
test("production spawn uses explicit Pi path and passes portable data root without rewriting PATH", async () => {
|
||||||
vi.stubEnv("PATH", "/usr/local/bin:/usr/bin");
|
vi.stubEnv("PATH", "/usr/local/bin:/usr/bin");
|
||||||
vi.stubEnv("PI_PROVIDER_API_KEY", "provider-secret");
|
vi.stubEnv("PI_PROVIDER_API_KEY", "provider-secret");
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ services:
|
|||||||
THT_DB_NAME: "${THT_DB_NAME:-}"
|
THT_DB_NAME: "${THT_DB_NAME:-}"
|
||||||
THT_DWH_REST_URL: "${THT_DWH_REST_URL:-}"
|
THT_DWH_REST_URL: "${THT_DWH_REST_URL:-}"
|
||||||
THT_VEC_REST_URL: "${THT_VEC_REST_URL:-}"
|
THT_VEC_REST_URL: "${THT_VEC_REST_URL:-}"
|
||||||
|
THT_VEC_WRITE_REST_URL: "${THT_VEC_WRITE_REST_URL:-}"
|
||||||
THT_OLLAMA_URL: "${THT_OLLAMA_URL:-}"
|
THT_OLLAMA_URL: "${THT_OLLAMA_URL:-}"
|
||||||
|
THT_PROFILE: "${THT_PROFILE:-server}"
|
||||||
THT_DOCS_ROOT: "${THT_DOCS_ROOT:-/data/workspaces/example/evidence-source}"
|
THT_DOCS_ROOT: "${THT_DOCS_ROOT:-/data/workspaces/example/evidence-source}"
|
||||||
THT_SECRETS_FILE: /run/secrets/thothii.secrets
|
THT_SECRETS_FILE: /run/secrets/thothii.secrets
|
||||||
THT_DATA_ROOT: /data
|
THT_DATA_ROOT: /data
|
||||||
@@ -31,6 +33,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- thoth_data:/data
|
- thoth_data:/data
|
||||||
- ./deploy/workspaces:/app/harness/workspaces:ro
|
- ./deploy/workspaces:/app/harness/workspaces:ro
|
||||||
|
- ./deploy/pi/models.json:/home/thoth/.pi/agent/models.json:ro
|
||||||
|
- ./deploy/pi/settings.json:/home/thoth/.pi/agent/settings.json:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [CMD, curl, --fail, --silent, http://127.0.0.1:8787/health]
|
test: [CMD, curl, --fail, --silent, http://127.0.0.1:8787/health]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
core:
|
||||||
|
environment:
|
||||||
|
THT_DOCS_ROOT: /data/workspaces/psd
|
||||||
|
extra_hosts:
|
||||||
|
- host.docker.internal:host-gateway
|
||||||
|
volumes:
|
||||||
|
- ./deploy/workspaces/psd.yaml:/app/harness/config/tht.yaml:ro
|
||||||
|
- type: bind
|
||||||
|
source: ${THT_PSD_WORKSPACE_HOST_PATH:?set THT_PSD_WORKSPACE_HOST_PATH}
|
||||||
|
target: /data/workspaces/psd
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"zai": {
|
||||||
|
"baseUrl": "https://api.z.ai/api/coding/paas/v4",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"apiKey": "$ZAI_API_KEY",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "glm-5.2",
|
||||||
|
"name": "GLM-5.2",
|
||||||
|
"reasoning": true,
|
||||||
|
"contextWindow": 200000,
|
||||||
|
"maxTokens": 131072
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"defaultProjectTrust": "always"
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
language: it
|
||||||
|
|
||||||
|
dwh:
|
||||||
|
type: thoth_rest
|
||||||
|
database:
|
||||||
|
database: ${THT_DB_NAME}
|
||||||
|
schema: datawarehouse
|
||||||
|
endpoint:
|
||||||
|
base_url: ${THT_DWH_REST_URL}
|
||||||
|
api_key: ${THT_DWH_API_KEY}
|
||||||
|
ssl_ca: ${THT_SSL_CA}
|
||||||
|
|
||||||
|
vectors:
|
||||||
|
type: thoth_vector_http
|
||||||
|
reader:
|
||||||
|
base_url: ${THT_VEC_REST_URL}
|
||||||
|
api_key: ${THT_VEC_API_KEY}
|
||||||
|
ssl_ca: ${THT_SSL_CA}
|
||||||
|
writer:
|
||||||
|
base_url: ${THT_VEC_WRITE_REST_URL}
|
||||||
|
api_key: ${THT_VEC_WRITE_API_KEY}
|
||||||
|
ssl_ca: ${THT_SSL_CA}
|
||||||
|
|
||||||
|
roots:
|
||||||
|
artifacts: /data/workspaces/psd/runtime-v2/artifacts
|
||||||
|
indexes: /data/workspaces/psd/runtime-v2/indexes
|
||||||
|
sessions: /data/workspaces/psd/sessions
|
||||||
|
|
||||||
|
evidence:
|
||||||
|
source_root: ${THT_DOCS_ROOT}
|
||||||
|
evidence_dir: evidence
|
||||||
|
|
||||||
|
embeddings:
|
||||||
|
base_url: ${THT_OLLAMA_URL}
|
||||||
|
model: nomic-embed-text-v2-moe
|
||||||
|
dim: 768
|
||||||
|
batch_size: 32
|
||||||
|
|
||||||
|
execution:
|
||||||
|
allow: [cte_test, explain, preview, aggregate, export]
|
||||||
|
max_preview_rows: 10
|
||||||
|
max_export_rows: 100000
|
||||||
|
statement_timeout_ms: 30000
|
||||||
@@ -24,8 +24,8 @@ RUN apt-get update \
|
|||||||
&& apt-get install --yes --no-install-recommends ca-certificates curl \
|
&& apt-get install --yes --no-install-recommends ca-certificates curl \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& useradd --create-home --uid 10001 thoth \
|
&& useradd --create-home --uid 10001 thoth \
|
||||||
&& mkdir -p /app/backend /app/docker/smoke /data/settings \
|
&& mkdir -p /app/backend /app/docker/smoke /app/harness/config /data/settings /home/thoth/.pi/agent \
|
||||||
&& chown -R thoth:thoth /data
|
&& chown -R thoth:thoth /app/harness /data /home/thoth/.pi
|
||||||
|
|
||||||
COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node
|
COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node
|
||||||
COPY --from=node-runtime /opt/pi-runtime /opt/pi-runtime
|
COPY --from=node-runtime /opt/pi-runtime /opt/pi-runtime
|
||||||
@@ -36,11 +36,13 @@ RUN ln -s /opt/pi-runtime/node_modules/.bin/pi /usr/local/bin/pi \
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY harness/ /app/harness/
|
COPY harness/ /app/harness/
|
||||||
COPY --from=gate-deps /src/harness/node_modules /app/harness/node_modules
|
COPY --from=gate-deps /src/harness/node_modules /app/harness/node_modules
|
||||||
|
RUN chown -R thoth:thoth /app/harness
|
||||||
COPY docker/python-runtime/requirements.lock /app/docker/python-runtime/requirements.lock
|
COPY docker/python-runtime/requirements.lock /app/docker/python-runtime/requirements.lock
|
||||||
RUN python -m venv /opt/venv \
|
RUN python -m venv /opt/venv \
|
||||||
&& /opt/venv/bin/pip install --no-cache-dir --require-hashes \
|
&& /opt/venv/bin/pip install --no-cache-dir --require-hashes \
|
||||||
--requirement /app/docker/python-runtime/requirements.lock \
|
--requirement /app/docker/python-runtime/requirements.lock \
|
||||||
&& /opt/venv/bin/pip install --no-cache-dir --no-deps --no-build-isolation '/app/harness[s3]' \
|
&& /opt/venv/bin/pip install --no-cache-dir --no-deps --no-build-isolation '/app/harness[s3]' \
|
||||||
|
&& cp /app/harness/workflow.yaml /opt/venv/lib/python3.12/site-packages/workflow.yaml \
|
||||||
&& /opt/venv/bin/tht vector migrate --help >/dev/null
|
&& /opt/venv/bin/tht vector migrate --help >/dev/null
|
||||||
|
|
||||||
COPY --from=backend-build /src/backend/dist /app/backend/dist
|
COPY --from=backend-build /src/backend/dist /app/backend/dist
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Local and Server Docker Deployment Implementation Plan
|
||||||
|
|
||||||
|
> **For Codex:** execute this plan in the current isolated worktree; keep runtime credentials out of Git.
|
||||||
|
|
||||||
|
**Goal:** Configure and verify a Docker Desktop deployment using GLM 5.2 and the existing PSD workspace, while retaining a portable server deployment contract.
|
||||||
|
|
||||||
|
**Architecture:** The base Compose file builds two applications and consumes only generic environment values and a Docker secret bundle. A tracked GLM Pi registry is mounted read-only in the core container. A Git-ignored local override supplies Mac-specific PSD workspace and CA mounts; server operators supply equivalent server runtime values separately.
|
||||||
|
|
||||||
|
**Tech Stack:** Docker Compose v2, Node 22, Python 3.12, Pi RPC, Fastify, nginx.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add the non-secret GLM Pi registry
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `deploy/pi/models.json`
|
||||||
|
- Modify: `docker/core.Dockerfile`
|
||||||
|
- Modify: `compose.yaml`
|
||||||
|
- Test: Compose configuration and Pi model discovery
|
||||||
|
|
||||||
|
1. Define the `zai/glm-5.2` OpenAI-compatible model registry without a credential.
|
||||||
|
2. Create the Pi user configuration directory in the core image and mount the registry read-only.
|
||||||
|
3. Verify that `get_available_models` returns `zai/glm-5.2` when the bundle supplies the model key.
|
||||||
|
|
||||||
|
### Task 2: Add generic PSD-compatible runtime templates
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `deploy/workspaces/psd.yaml.example`
|
||||||
|
- Create: `deploy/compose.psd-local.yaml.example`
|
||||||
|
- Modify: `deploy/env.example`
|
||||||
|
- Modify: `README.md`
|
||||||
|
|
||||||
|
1. Define a relative `/data/workspaces/psd` workspace configuration with external REST DWH/vector adapters.
|
||||||
|
2. Document required non-secret environment values and the local/server boundary.
|
||||||
|
3. Keep host paths and credential values out of all tracked files.
|
||||||
|
|
||||||
|
### Task 3: Materialize local runtime configuration securely
|
||||||
|
|
||||||
|
**Files (ignored):**
|
||||||
|
- Create: `.env`
|
||||||
|
- Create: `deploy/secrets/thothii.secrets`
|
||||||
|
- Create: `deploy/compose.psd-local.yaml`
|
||||||
|
- Create: `deploy/workspaces/psd.yaml`
|
||||||
|
|
||||||
|
1. Transfer only required values from the existing local configuration without writing them to logs.
|
||||||
|
2. Set `PI_PROVIDER=zai`, `PI_MODEL=glm-5.2`, and the Docker Desktop host gateway for Ollama.
|
||||||
|
3. Bind-mount the PSD workspace and private CA read-only where appropriate; sessions remain writable.
|
||||||
|
4. Enforce restricted modes on the secret bundle.
|
||||||
|
|
||||||
|
### Task 4: Build and verify the Docker deployment
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `docker compose config --quiet`
|
||||||
|
- `docker compose build`
|
||||||
|
- `docker compose up -d`
|
||||||
|
- health/API/model/session smoke checks
|
||||||
|
|
||||||
|
1. Validate rendered Compose configuration without exposing secrets.
|
||||||
|
2. Build the core and frontend images.
|
||||||
|
3. Verify secret mount, core and frontend health, and model listing.
|
||||||
|
4. Start a PSD session using GLM 5.2 and verify Pi emits a workflow event or gate.
|
||||||
|
5. Capture sanitized diagnostics and stop only disposable test resources; leave the validated local stack running unless it fails.
|
||||||
|
|
||||||
|
### Task 5: Record the deployment result
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `README.md` or deployment documentation
|
||||||
|
|
||||||
|
1. Record the exact local startup command and server-equivalent configuration steps.
|
||||||
|
2. State verified endpoints, model, and session-start result without secret values.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
source_env=${1:-"$root/../../harness/.env"}
|
||||||
|
workspace=${2:-"$root/../../../tht-workspace-psd"}
|
||||||
|
auth_file=${3:-"$HOME/.pi/agent/auth.json"}
|
||||||
|
|
||||||
|
value() {
|
||||||
|
awk -F= -v key="$1" '$1 == key { sub(/^[^=]*=/, ""); sub(/[[:space:]].*$/, ""); print; exit }' "$source_env"
|
||||||
|
}
|
||||||
|
required() {
|
||||||
|
result=$(value "$1")
|
||||||
|
[ -n "$result" ] || { echo "missing $1 in local source configuration" >&2; exit 2; }
|
||||||
|
printf '%s' "$result"
|
||||||
|
}
|
||||||
|
|
||||||
|
test -f "$source_env"
|
||||||
|
test -d "$workspace"
|
||||||
|
test -f "$auth_file"
|
||||||
|
ca=$(value THT_SSL_CA)
|
||||||
|
[ -z "$ca" ] || test -f "$ca"
|
||||||
|
model_key=$(jq -er '.zai.key' "$auth_file")
|
||||||
|
test -n "$model_key"
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
mkdir -p "$root/deploy/secrets" "$root/deploy/workspaces"
|
||||||
|
cp "$root/deploy/compose.psd-local.yaml.example" "$root/deploy/compose.psd-local.yaml"
|
||||||
|
cp "$root/deploy/workspaces/psd.yaml.example" "$root/deploy/workspaces/psd.yaml"
|
||||||
|
cat >"$root/.env" <<EOF
|
||||||
|
COMPOSE_FILE=compose.yaml:deploy/compose.psd-local.yaml
|
||||||
|
COMPOSE_PROFILES=
|
||||||
|
THT_SECRETS_FILE=deploy/secrets/thothii.secrets
|
||||||
|
THOTH_HTTP_PORT=8080
|
||||||
|
AUTH_MODE=none
|
||||||
|
THOTH_PUBLIC_EXPOSURE=false
|
||||||
|
MAX_PI_PROCESSES=4
|
||||||
|
PI_PROVIDER=zai
|
||||||
|
PI_MODEL=glm-5.2
|
||||||
|
PI_THINKING=medium
|
||||||
|
THT_PROFILE=workstation
|
||||||
|
THT_DB_NAME=$(required THT_DB_NAME)
|
||||||
|
THT_DWH_REST_URL=$(required THT_DWH_REST_URL)
|
||||||
|
THT_VEC_REST_URL=$(required THT_VEC_REST_URL)
|
||||||
|
THT_VEC_WRITE_REST_URL=$(required THT_VEC_WRITE_REST_URL)
|
||||||
|
THT_OLLAMA_URL=http://host.docker.internal:11434
|
||||||
|
THT_DOCS_ROOT=/data/workspaces/psd
|
||||||
|
THT_PSD_WORKSPACE_HOST_PATH=$workspace
|
||||||
|
EOF
|
||||||
|
cat >"$root/deploy/secrets/thothii.secrets" <<EOF
|
||||||
|
THT_MODEL_API_KEY=$model_key
|
||||||
|
THT_DWH_API_KEY=$(required THT_DWH_API_KEY)
|
||||||
|
THT_VEC_API_KEY=$(required THT_VEC_API_KEY)
|
||||||
|
THT_VEC_WRITE_API_KEY=$(required THT_VEC_WRITE_API_KEY)
|
||||||
|
EOF
|
||||||
|
if [ -n "$ca" ]; then
|
||||||
|
cat >>"$root/deploy/compose.psd-local.yaml" <<EOF
|
||||||
|
- type: bind
|
||||||
|
source: $ca
|
||||||
|
target: /run/secrets/ca-chain.pem
|
||||||
|
read_only: true
|
||||||
|
EOF
|
||||||
|
printf '%s\n' 'THT_CA=/run/secrets/ca-chain.pem' >>"$root/deploy/secrets/thothii.secrets"
|
||||||
|
else
|
||||||
|
printf '%s\n' 'THT_CA=/etc/ssl/certs/ca-certificates.crt' >>"$root/deploy/secrets/thothii.secrets"
|
||||||
|
fi
|
||||||
|
chmod 600 "$root/.env" "$root/deploy/secrets/thothii.secrets"
|
||||||
|
echo "Local PSD Docker configuration materialized without printing secret values."
|
||||||
@@ -25,6 +25,9 @@ grep -q 'host_ip: 127.0.0.1' "$tmp/base.yaml"
|
|||||||
grep -q 'AUTH_MODE: none' "$tmp/base.yaml"
|
grep -q 'AUTH_MODE: none' "$tmp/base.yaml"
|
||||||
grep -q 'THOTH_PUBLIC_EXPOSURE: "false"' "$tmp/base.yaml"
|
grep -q 'THOTH_PUBLIC_EXPOSURE: "false"' "$tmp/base.yaml"
|
||||||
grep -q 'THT_SECRETS_FILE: /run/secrets/thothii.secrets' "$tmp/base.yaml"
|
grep -q 'THT_SECRETS_FILE: /run/secrets/thothii.secrets' "$tmp/base.yaml"
|
||||||
|
grep -q 'target: /home/thoth/.pi/agent/models.json' "$tmp/base.yaml"
|
||||||
|
grep -q 'source: .*/deploy/pi/models.json' "$tmp/base.yaml"
|
||||||
|
grep -q 'target: /home/thoth/.pi/agent/settings.json' "$tmp/base.yaml"
|
||||||
if grep -q 'THT_[A-Z0-9_]*_SECRET_FILE:' "$tmp/base.yaml"; then
|
if grep -q 'THT_[A-Z0-9_]*_SECRET_FILE:' "$tmp/base.yaml"; then
|
||||||
echo "base Compose must not require legacy secret-file variables" >&2
|
echo "base Compose must not require legacy secret-file variables" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Reference in New Issue
Block a user