fix(docker): run real questions through trusted Pi gate

This commit is contained in:
2026-07-12 19:20:10 +02:00
parent c446d40e1f
commit 7628eaa579
16 changed files with 266 additions and 14 deletions
+4 -1
View File
@@ -56,7 +56,10 @@ function unavailable(): Error { return new Error("secret bundle is unavailable")
function secureStat(info: Stats, docker: boolean): boolean {
const mode = info.mode & 0o777;
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);
}
+10 -2
View File
@@ -44,6 +44,14 @@ export class PiProcessManager {
credentialFile: this.cfg.modelApiKeyFile,
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;
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.
@@ -62,7 +70,7 @@ export class PiProcessManager {
async spawnFor(
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> {
// 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
@@ -110,7 +118,7 @@ export class PiProcessManager {
const message = o.mode === "resume"
? `/riprendi-sessione ${sessionId}`
: `/nuova-domanda "kickoff"`;
: `/nuova-domanda ${JSON.stringify(o.question ?? "")}`;
rpc.send({ type: "prompt", message });
return rt;
}
+5 -4
View File
@@ -29,12 +29,13 @@ export function sessionRoutes(
model: s.model,
thinking: s.thinking,
author: getUser(req).id,
question: b.question,
});
rt.bridge.onClientEvent((e) => d.hub.publish(id, e.type, e));
return { id };
});
app.get("/sessions", async () => d.tht.sessionList());
app.get("/sessions/:id", async (req) => d.tht.sessionShow((req.params as any).id));
app.get("/sessions", async () => d.tht.sessionList(d.getSettings().workspace));
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) => {
const id = (req.params as any).id;
const rt = d.mgr.get(id);
@@ -50,7 +51,7 @@ export function sessionRoutes(
});
app.post("/sessions/:id/resume", async (req, reply) => {
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) {
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) => {
const id = (req.params as any).id;
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();
});
app.get("/sessions/:id/documents", async (req) => d.tht.documents((req.params as any).id));
+8 -5
View File
@@ -109,12 +109,12 @@ export class ThtRunner {
return this.json<{ id: string }>(a, o.workspace);
}
sessionList() {
return this.json<SessionRow[]>(["session", "list", "--json"]);
sessionList(workspace?: string) {
return this.json<SessionRow[]>(["session", "list", "--json"], workspace);
}
sessionShow(id: string) {
return this.json<unknown>(["session", "show", id, "--json"]);
sessionShow(id: string, workspace?: string) {
return this.json<unknown>(["session", "show", id, "--json"], workspace);
}
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]); }
archive(id: string) { return this.ok(["session", "archive", 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"]); }
async ollamaEnsure(workspace: string, timeoutSec: number): Promise<OllamaEnsureResult> {