Fix session resume and PSD container configuration
This commit is contained in:
@@ -40,6 +40,8 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
|
|||||||
harnessDir: config.harnessDir,
|
harnessDir: config.harnessDir,
|
||||||
configPath: process.env.THT_CONFIG ?? "config/tht.yaml",
|
configPath: process.env.THT_CONFIG ?? "config/tht.yaml",
|
||||||
dataRoot: config.dataRoot,
|
dataRoot: config.dataRoot,
|
||||||
|
secretsFile: config.secretsFile,
|
||||||
|
secretFiles: config.secretFiles,
|
||||||
});
|
});
|
||||||
const mgr = deps?.mgr ?? new PiProcessManager(config, deps?.spawnFn ? { spawnFn: deps.spawnFn } : undefined);
|
const mgr = deps?.mgr ?? new PiProcessManager(config, deps?.spawnFn ? { spawnFn: deps.spawnFn } : undefined);
|
||||||
const hub = deps?.hub ?? new SseHub();
|
const hub = deps?.hub ?? new SseHub();
|
||||||
|
|||||||
@@ -68,9 +68,18 @@ export class PiProcessManager {
|
|||||||
// this managed session process the adapter values already loaded by the core
|
// this managed session process the adapter values already loaded by the core
|
||||||
// entrypoint; the generic provider helper continues to scrub them by default.
|
// entrypoint; the generic provider helper continues to scrub them by default.
|
||||||
for (const name of [
|
for (const name of [
|
||||||
"THT_DWH_API_KEY", "THT_VEC_API_KEY", "THT_VEC_WRITE_API_KEY", "THT_SSL_CA",
|
"THT_DWH_API_KEY", "THT_VEC_API_KEY", "THT_VEC_WRITE_API_KEY",
|
||||||
] as const) {
|
] as const) {
|
||||||
if (process.env[name] !== undefined) env[name] = process.env[name];
|
const value = secretValue(this.cfg, name) ?? process.env[name];
|
||||||
|
if (value !== undefined) env[name] = value;
|
||||||
|
}
|
||||||
|
const ca = secretValue(this.cfg, "THT_SSL_CA")
|
||||||
|
?? secretValue(this.cfg, "THT_CA")
|
||||||
|
?? process.env.THT_SSL_CA
|
||||||
|
?? process.env.THT_CA;
|
||||||
|
if (ca !== undefined) {
|
||||||
|
env.THT_CA = ca;
|
||||||
|
env.THT_SSL_CA = ca;
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { spawn } from "node:child_process";
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { clearPrincipalEnvironment, principalEnvironment, type PrincipalContext } from "../auth/principal.js";
|
import { clearPrincipalEnvironment, principalEnvironment, type PrincipalContext } from "../auth/principal.js";
|
||||||
|
import { secretValue, type SecretBundleConfig } from "../config/secret-bundle.js";
|
||||||
|
|
||||||
export interface ThtConfig {
|
export interface ThtConfig extends SecretBundleConfig {
|
||||||
thtBin: string;
|
thtBin: string;
|
||||||
harnessDir: string;
|
harnessDir: string;
|
||||||
configPath: string;
|
configPath: string;
|
||||||
@@ -82,6 +83,17 @@ export class ThtRunner {
|
|||||||
clearPrincipalEnvironment(env);
|
clearPrincipalEnvironment(env);
|
||||||
if (this.cfg.dataRoot !== undefined) env.THT_DATA_ROOT = this.cfg.dataRoot;
|
if (this.cfg.dataRoot !== undefined) env.THT_DATA_ROOT = this.cfg.dataRoot;
|
||||||
if (this.principal) Object.assign(env, principalEnvironment(this.principal));
|
if (this.principal) Object.assign(env, principalEnvironment(this.principal));
|
||||||
|
for (const name of [
|
||||||
|
"THT_DWH_API_KEY", "THT_VEC_API_KEY", "THT_VEC_WRITE_API_KEY",
|
||||||
|
] as const) {
|
||||||
|
const value = secretValue(this.cfg, name);
|
||||||
|
if (value !== undefined) env[name] = value;
|
||||||
|
}
|
||||||
|
const ca = secretValue(this.cfg, "THT_SSL_CA") ?? secretValue(this.cfg, "THT_CA");
|
||||||
|
if (ca !== undefined) {
|
||||||
|
env.THT_CA = ca;
|
||||||
|
env.THT_SSL_CA = ca;
|
||||||
|
}
|
||||||
const ch = spawn(this.cfg.thtBin, this.buildArgv(args, workspace), {
|
const ch = spawn(this.cfg.thtBin, this.buildArgv(args, workspace), {
|
||||||
cwd: this.cfg.harnessDir,
|
cwd: this.cfg.harnessDir,
|
||||||
env,
|
env,
|
||||||
|
|||||||
@@ -350,7 +350,14 @@ test("skips managed-key injection for a provider present in pi's auth store", as
|
|||||||
|
|
||||||
test("session Pi spawn reads the single secret bundle and scrubs its path", async () => {
|
test("session Pi spawn reads the single secret bundle and scrubs its path", async () => {
|
||||||
const secret = path.resolve(__dirname, `.bundle-${process.pid}`);
|
const secret = path.resolve(__dirname, `.bundle-${process.pid}`);
|
||||||
writeFileSync(secret, "THT_MODEL_API_KEY=bundle-secret\n", { mode: 0o600 });
|
writeFileSync(secret, [
|
||||||
|
"THT_MODEL_API_KEY=bundle-secret",
|
||||||
|
"THT_DWH_API_KEY=dwh-secret",
|
||||||
|
"THT_VEC_API_KEY=vector-reader-secret",
|
||||||
|
"THT_VEC_WRITE_API_KEY=vector-writer-secret",
|
||||||
|
"THT_CA=/run/secrets/ca-chain.pem",
|
||||||
|
"",
|
||||||
|
].join("\n"), { mode: 0o600 });
|
||||||
chmodSync(secret, 0o600);
|
chmodSync(secret, 0o600);
|
||||||
const calls: any[][] = [];
|
const calls: any[][] = [];
|
||||||
const child = recordingChild();
|
const child = recordingChild();
|
||||||
@@ -361,6 +368,13 @@ test("session Pi spawn reads the single secret bundle and scrubs its path", asyn
|
|||||||
try {
|
try {
|
||||||
await mgr.spawnFor("bundle-session", { provider: "openai" });
|
await mgr.spawnFor("bundle-session", { provider: "openai" });
|
||||||
expect(calls[0][2].env.OPENAI_API_KEY).toBe("bundle-secret");
|
expect(calls[0][2].env.OPENAI_API_KEY).toBe("bundle-secret");
|
||||||
|
expect(calls[0][2].env).toMatchObject({
|
||||||
|
THT_DWH_API_KEY: "dwh-secret",
|
||||||
|
THT_VEC_API_KEY: "vector-reader-secret",
|
||||||
|
THT_VEC_WRITE_API_KEY: "vector-writer-secret",
|
||||||
|
THT_CA: "/run/secrets/ca-chain.pem",
|
||||||
|
THT_SSL_CA: "/run/secrets/ca-chain.pem",
|
||||||
|
});
|
||||||
expect(calls[0][2].env).not.toHaveProperty("THT_SECRETS_FILE");
|
expect(calls[0][2].env).not.toHaveProperty("THT_SECRETS_FILE");
|
||||||
} finally {
|
} finally {
|
||||||
mgr.teardown("bundle-session");
|
mgr.teardown("bundle-session");
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { test, expect, vi } from "vitest";
|
import { test, expect, vi } from "vitest";
|
||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
|
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
import { ThtRunner } from "../src/tht/tht-runner.js";
|
import { ThtRunner } from "../src/tht/tht-runner.js";
|
||||||
|
|
||||||
// Spy on child_process.spawn so we can capture the resolved argv (incl. -c config)
|
// Spy on child_process.spawn so we can capture the resolved argv (incl. -c config)
|
||||||
@@ -63,6 +66,41 @@ test("run passes configured THT_DATA_ROOT and preserves the remaining environmen
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("run injects DWH/vector credentials from the mounted secret bundle", async () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "tht-runner-bundle-"));
|
||||||
|
const secret = join(dir, "thothii.secrets");
|
||||||
|
writeFileSync(secret, [
|
||||||
|
"THT_DWH_API_KEY=dwh-secret",
|
||||||
|
"THT_VEC_API_KEY=vector-reader-secret",
|
||||||
|
"THT_VEC_WRITE_API_KEY=vector-writer-secret",
|
||||||
|
"THT_CA=/run/secrets/ca-chain.pem",
|
||||||
|
"",
|
||||||
|
].join("\n"), { mode: 0o600 });
|
||||||
|
chmodSync(secret, 0o600);
|
||||||
|
try {
|
||||||
|
(spawn as any).mockClear();
|
||||||
|
const runner = new ThtRunner({
|
||||||
|
thtBin: "tht",
|
||||||
|
harnessDir: "/app/harness",
|
||||||
|
configPath: "config/tht.yaml",
|
||||||
|
secretsFile: secret,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
await runner.run(["session", "list", "--json"]);
|
||||||
|
|
||||||
|
const env = (spawn as any).mock.calls[0][2].env;
|
||||||
|
expect(env).toMatchObject({
|
||||||
|
THT_DWH_API_KEY: "dwh-secret",
|
||||||
|
THT_VEC_API_KEY: "vector-reader-secret",
|
||||||
|
THT_VEC_WRITE_API_KEY: "vector-writer-secret",
|
||||||
|
THT_CA: "/run/secrets/ca-chain.pem",
|
||||||
|
THT_SSL_CA: "/run/secrets/ca-chain.pem",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("run omits ambient THT_DATA_ROOT when config does not provide one", async () => {
|
test("run omits ambient THT_DATA_ROOT when config does not provide one", async () => {
|
||||||
const previousDataRoot = process.env.THT_DATA_ROOT;
|
const previousDataRoot = process.env.THT_DATA_ROOT;
|
||||||
const previousCredential = process.env.PI_PROVIDER_API_KEY;
|
const previousCredential = process.env.PI_PROVIDER_API_KEY;
|
||||||
|
|||||||
@@ -1,11 +1,41 @@
|
|||||||
services:
|
services:
|
||||||
core:
|
core:
|
||||||
environment:
|
environment:
|
||||||
|
AUTH_MODE: ${AUTH_MODE:-none}
|
||||||
|
THOTH_PUBLIC_EXPOSURE: ${THOTH_PUBLIC_EXPOSURE:-false}
|
||||||
|
MAX_PI_PROCESSES: ${MAX_PI_PROCESSES:-4}
|
||||||
|
PI_PROVIDER: ${PI_PROVIDER:?set PI_PROVIDER}
|
||||||
|
PI_MODEL: ${PI_MODEL:?set PI_MODEL}
|
||||||
|
PI_THINKING: ${PI_THINKING:-medium}
|
||||||
|
THT_PROFILE: ${THT_PROFILE:-workstation}
|
||||||
|
THT_DB_NAME: ${THT_DB_NAME:?set THT_DB_NAME}
|
||||||
|
THT_DWH_REST_URL: ${THT_DWH_REST_URL:?set THT_DWH_REST_URL}
|
||||||
|
THT_VEC_REST_URL: ${THT_VEC_REST_URL:?set THT_VEC_REST_URL}
|
||||||
|
THT_VEC_WRITE_REST_URL: ${THT_VEC_WRITE_REST_URL:?set THT_VEC_WRITE_REST_URL}
|
||||||
|
THT_OLLAMA_URL: ${THT_OLLAMA_URL:?set THT_OLLAMA_URL}
|
||||||
|
THT_SECRETS_FILE: /run/secrets/thothii.secrets
|
||||||
THT_DOCS_ROOT: /data/workspaces/psd
|
THT_DOCS_ROOT: /data/workspaces/psd
|
||||||
|
THT_CONFIG: /app/harness/config/tht.yaml
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- host.docker.internal:host-gateway
|
- host.docker.internal:host-gateway
|
||||||
|
networks: !override
|
||||||
|
default:
|
||||||
|
aliases: [core, thothii-core]
|
||||||
volumes:
|
volumes:
|
||||||
|
- thoth_data:/data
|
||||||
|
- thoth_pi_config:/home/thoth/.pi
|
||||||
|
- ./deploy/pi/models.json:/home/thoth/.pi/agent/models.json:ro
|
||||||
|
- ./deploy/pi/settings.json:/home/thoth/.pi/agent/settings.json:ro
|
||||||
|
- ${THT_SECRETS_FILE:?set THT_SECRETS_FILE}:/run/secrets/thothii.secrets:ro
|
||||||
|
- ${THT_PSD_WORKSPACE_HOST_PATH:?set THT_PSD_WORKSPACE_HOST_PATH}/evidence:/data/evidence:ro
|
||||||
- ./deploy/workspaces/psd.yaml:/app/harness/config/tht.yaml:ro
|
- ./deploy/workspaces/psd.yaml:/app/harness/config/tht.yaml:ro
|
||||||
- type: bind
|
- ${THT_PSD_WORKSPACE_HOST_PATH:?set THT_PSD_WORKSPACE_HOST_PATH}:/data/workspaces/psd
|
||||||
source: ${THT_PSD_WORKSPACE_HOST_PATH:?set THT_PSD_WORKSPACE_HOST_PATH}
|
|
||||||
target: /data/workspaces/psd
|
volumes:
|
||||||
|
thoth_data:
|
||||||
|
thoth_pi_config:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external: true
|
||||||
|
name: thothii_default
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
|||||||
|
|
||||||
# Utente non-root
|
# Utente non-root
|
||||||
RUN useradd --create-home --uid 10001 --shell /bin/bash thoth
|
RUN useradd --create-home --uid 10001 --shell /bin/bash thoth
|
||||||
|
RUN mkdir -p /home/thoth/.pi/agent && chown -R thoth:thoth /home/thoth/.pi
|
||||||
|
|
||||||
COPY harness/ /app/harness/
|
COPY harness/ /app/harness/
|
||||||
# Pi scrive lock/settings in .pi: ownership thoth per sopravvivere al rebuild
|
# Pi scrive lock/settings in .pi: ownership thoth per sopravvivere al rebuild
|
||||||
@@ -43,8 +44,11 @@ RUN python -m venv /opt/venv \
|
|||||||
&& /opt/venv/bin/pip install --no-cache-dir --upgrade pip \
|
&& /opt/venv/bin/pip install --no-cache-dir --upgrade pip \
|
||||||
&& (cd /app/harness && /opt/venv/bin/pip install --no-cache-dir .) \
|
&& (cd /app/harness && /opt/venv/bin/pip install --no-cache-dir .) \
|
||||||
&& cp /app/harness/workflow.yaml /opt/venv/lib/python3.12/site-packages/workflow.yaml
|
&& cp /app/harness/workflow.yaml /opt/venv/lib/python3.12/site-packages/workflow.yaml
|
||||||
# tht cerca config/tht.yaml relativo al CWD (ignora THT_CONFIG env). Symlink al workspace attivo.
|
# Default locale e alias PSD convergono sul file canonico. Il CLI onora anche
|
||||||
RUN mkdir -p /app/harness/config && ln -sf /app/harness/workspaces/local.yaml /app/harness/config/tht.yaml
|
# THT_CONFIG, quindi cambiare CWD non cambia l'identita' dello workspace.
|
||||||
|
RUN mkdir -p /app/harness/config \
|
||||||
|
&& cp --remove-destination /app/harness/workspaces/local.yaml /app/harness/config/tht.yaml \
|
||||||
|
&& ln -sfn /app/harness/config/tht.yaml /app/harness/workspaces/psd.yaml
|
||||||
# PiProcessManager (backend) prepende harnessDir/.venv/bin al PATH del child Pi → symlink al venv reale
|
# PiProcessManager (backend) prepende harnessDir/.venv/bin al PATH del child Pi → symlink al venv reale
|
||||||
RUN ln -s /opt/venv /app/harness/.venv
|
RUN ln -s /opt/venv /app/harness/.venv
|
||||||
|
|
||||||
|
|||||||
@@ -245,3 +245,61 @@ test("reviewer_schema_linking returns a textResult (not a throw) when a proposed
|
|||||||
_handler = null;
|
_handler = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("reviewer_schema_linking explains that phase four stays open when joins are missing", async () => {
|
||||||
|
_handler = (_file, args) => {
|
||||||
|
if (args[0] === "phase" && args[1] === "meta")
|
||||||
|
return JSON.stringify({ phases: [{ num: 4, id: "F4" }] });
|
||||||
|
if (args[0] === "phase" && args[1] === "show") return "Fase corrente: 4\n";
|
||||||
|
if (args[0] === "schema" && args[1] === "columns")
|
||||||
|
return JSON.stringify({ ...CATALOG, table: args[2] });
|
||||||
|
if (args[0] === "phase" && args[1] === "advance") {
|
||||||
|
const error = new Error("phase advance failed");
|
||||||
|
error.stderr = "Fase 4: più tabelle promosse richiedono join strutturati";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gate = require(GATE);
|
||||||
|
const { createFakePi } = require("./fake_pi_runtime.js");
|
||||||
|
const { pi, ctx, tools } = createFakePi();
|
||||||
|
ctx.cwd = "/nonexistent-thothii-test-cwd-open-f4";
|
||||||
|
gate.default(pi);
|
||||||
|
|
||||||
|
ctx.ui.input = async (title) => {
|
||||||
|
const descriptor = JSON.parse(title);
|
||||||
|
return JSON.stringify({
|
||||||
|
id: descriptor.id,
|
||||||
|
kind: "schema-linking",
|
||||||
|
tables: descriptor.tables.map((table) => ({
|
||||||
|
id: table.id,
|
||||||
|
enacted: true,
|
||||||
|
columns: ["cod_paz"],
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await tools.get("reviewer_schema_linking").def.execute(
|
||||||
|
"call-open-f4",
|
||||||
|
{
|
||||||
|
session: "s1",
|
||||||
|
title: "Schema linking",
|
||||||
|
tables: [
|
||||||
|
{ id: "fact", name: "fact_event", kind: "promote", suggested_columns: ["cod_paz"] },
|
||||||
|
{ id: "patient", name: "dim_patient", kind: "promote", suggested_columns: ["cod_paz"] },
|
||||||
|
],
|
||||||
|
advance: true,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(result.content[0].text, /Fase resta aperta/i);
|
||||||
|
assert.match(result.content[0].text, /join/i);
|
||||||
|
} finally {
|
||||||
|
_handler = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const cp = require("node:child_process");
|
||||||
|
const { createRequire } = require("node:module");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
const GATE = path.join(__dirname, "..", "..", "tht-gate.js");
|
||||||
|
|
||||||
|
if (typeof globalThis.require === "undefined") {
|
||||||
|
globalThis.require = createRequire(GATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("writing reviewed structured joins closes phase four", async () => {
|
||||||
|
const calls = [];
|
||||||
|
const original = cp.execFileSync;
|
||||||
|
cp.execFileSync = (_file, args) => {
|
||||||
|
calls.push(args.join(" "));
|
||||||
|
if (args[0] === "phase" && args[1] === "show") return "Fase corrente: 4\n";
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gate = require(GATE);
|
||||||
|
const { createFakePi } = require("./fake_pi_runtime.js");
|
||||||
|
const { pi, ctx, tools } = createFakePi();
|
||||||
|
ctx.cwd = "/nonexistent-thothii-test-cwd";
|
||||||
|
gate.default(pi);
|
||||||
|
|
||||||
|
const result = await tools.get("write_schema_linking").def.execute(
|
||||||
|
"call-write-linking",
|
||||||
|
{
|
||||||
|
session: "s1",
|
||||||
|
schema_linking: {
|
||||||
|
question: "q",
|
||||||
|
candidates: [
|
||||||
|
{ kind: "table", name: "fact_event", decision: "promoted" },
|
||||||
|
{ kind: "table", name: "dim_patient", decision: "promoted" },
|
||||||
|
],
|
||||||
|
joins: [{ from: "fact_event.cod_paz", to: "dim_patient.cod_paz" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(calls.includes("session set-schema-linking s1 --file -"));
|
||||||
|
assert.ok(calls.includes("phase advance --session s1"));
|
||||||
|
assert.match(result.content[0].text, /Fase avanzata automaticamente/);
|
||||||
|
} finally {
|
||||||
|
cp.execFileSync = original;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1117,6 +1117,7 @@ export default function (pi) {
|
|||||||
const adv = advance ? forceAdvance(ctx, session) : { advanced: false };
|
const adv = advance ? forceAdvance(ctx, session) : { advanced: false };
|
||||||
const parts = [`Schema linking registrato dal reviewer (${n} tabelle + colonne curate). schema_linking.json scritto.`];
|
const parts = [`Schema linking registrato dal reviewer (${n} tabelle + colonne curate). schema_linking.json scritto.`];
|
||||||
if (adv.advanced) parts.push("Fase avanzata automaticamente — nessun gate aggiuntivo necessario.");
|
if (adv.advanced) parts.push("Fase avanzata automaticamente — nessun gate aggiuntivo necessario.");
|
||||||
|
else if (advance && adv.error) parts.push(`Fase resta aperta: ${adv.error}`);
|
||||||
return textResult(parts.join(" "));
|
return textResult(parts.join(" "));
|
||||||
} catch (fatal) {
|
} catch (fatal) {
|
||||||
const msg = (fatal.stderr || fatal.message || String(fatal)).toString().trim();
|
const msg = (fatal.stderr || fatal.message || String(fatal)).toString().trim();
|
||||||
@@ -1636,9 +1637,13 @@ export default function (pi) {
|
|||||||
["session", "set-schema-linking", session, "--file", "-"],
|
["session", "set-schema-linking", session, "--file", "-"],
|
||||||
JSON.stringify(schema_linking),
|
JSON.stringify(schema_linking),
|
||||||
);
|
);
|
||||||
return textResult(
|
const message = `schema_linking.json scritto e validato per la sessione ${session}.`;
|
||||||
`schema_linking.json scritto e validato per la sessione ${session}.`,
|
if (currentPhase(ctx, session) !== 4) return textResult(message);
|
||||||
);
|
const advanced = forceAdvance(ctx, session);
|
||||||
|
if (advanced.error) {
|
||||||
|
return textResult(`${message} Fase non avanzata: ${advanced.error}`);
|
||||||
|
}
|
||||||
|
return textResult(`${message} Fase avanzata automaticamente.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const cliMsg = (e.stderr || e.message || String(e)).toString().trim();
|
const cliMsg = (e.stderr || e.message || String(e)).toString().trim();
|
||||||
return textResult(`${cliMsg} Correggi schema_linking e riprova.`);
|
return textResult(`${cliMsg} Correggi schema_linking e riprova.`);
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ closes the phase itself; a `reviewer_confirm kind:"phase"` summary gate exists o
|
|||||||
completeness is a human judgment (F1, F2 with recorded memories, F5). A `reviewer_decide`/
|
completeness is a human judgment (F1, F2 with recorded memories, F5). A `reviewer_decide`/
|
||||||
`reviewer_select` choice records its OWN decision but does NOT advance the phase. `advance:true`
|
`reviewer_select` choice records its OWN decision but does NOT advance the phase. `advance:true`
|
||||||
on `reviewer_decide` auto-advances only F2 (empty memory) and F6 (skipped/empty) — never a
|
on `reviewer_decide` auto-advances only F2 (empty memory) and F6 (skipped/empty) — never a
|
||||||
phase that recorded substantive decisions. FIVE gates close their phase themselves, because
|
phase that recorded substantive decisions. FIVE phase-completion mechanisms close their phase
|
||||||
there the human interaction IS the phase approval: `rewrite_question` (F3),
|
themselves, because there the human interaction IS the phase approval: `rewrite_question` (F3),
|
||||||
`reviewer_schema_linking` with `advance:true` (F4), the LAST `reviewer_confirm
|
the final F4 schema persistence (`reviewer_schema_linking` for a single-table plan, otherwise
|
||||||
|
`write_schema_linking` after the join review), the LAST `reviewer_confirm
|
||||||
kind:"cte_result"` of the plan (F6), `reviewer_confirm kind:"sql"` (F7), and
|
kind:"cte_result"` of the plan (F6), `reviewer_confirm kind:"sql"` (F7), and
|
||||||
`reviewer_memory_promote` (F8).
|
`reviewer_memory_promote` (F8).
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ kind:"cte_result"` of the plan (F6), `reviewer_confirm kind:"sql"` (F7), and
|
|||||||
| F1 chiarimento | — | `reviewer_confirm kind:"phase"` |
|
| F1 chiarimento | — | `reviewer_confirm kind:"phase"` |
|
||||||
| F2 memoria | — | `advance:true` only if nothing recorded; else `reviewer_confirm kind:"phase"` |
|
| F2 memoria | — | `advance:true` only if nothing recorded; else `reviewer_confirm kind:"phase"` |
|
||||||
| F3 riscrittura | `question.md` | `rewrite_question` records approval and advances automatically |
|
| F3 riscrittura | `question.md` | `rewrite_question` records approval and advances automatically |
|
||||||
| F4 schema_linking | `schema_linking.json` | `reviewer_schema_linking` with `advance:true` closes the phase itself (the curation IS the approval; `reviewer_confirm kind:"phase"` only as fallback if it reports an error). Promoted columns are the reviewer-approved OUTPUT columns — project exactly those in the final SELECT. |
|
| F4 schema_linking | `schema_linking.json` | `reviewer_schema_linking(advance:true)` closes a single-table plan. With multiple promoted tables it deliberately keeps F4 open until the separate join review is persisted into `schema_linking.json`; the succeeding `write_schema_linking` closes F4 automatically. Never add `reviewer_confirm kind:"phase"`. Promoted columns are the reviewer-approved OUTPUT columns — project exactly those in the final SELECT. |
|
||||||
| F5 sintesi | — | `reviewer_confirm kind:"phase"` (after `tht session check`) |
|
| F5 sintesi | — | `reviewer_confirm kind:"phase"` (after `tht session check`) |
|
||||||
| F6 cte | `cte_plan.json`, `ctes/`, `cte_tests.json` | approve each CTE with `kind:"cte_result"`; approving the LAST CTE of the plan closes the phase automatically (`kind:"phase"` only as fallback if the auto-close reports an error) |
|
| F6 cte | `cte_plan.json`, `ctes/`, `cte_tests.json` | approve each CTE with `kind:"cte_result"`; approving the LAST CTE of the plan closes the phase automatically (`kind:"phase"` only as fallback if the auto-close reports an error) |
|
||||||
| F7 sql_finale | `sql_final.sql` | `kind:"sql"` records `sql_approved` AND closes the phase automatically (`kind:"phase"` only as fallback if it reports an error) |
|
| F7 sql_finale | `sql_final.sql` | `kind:"sql"` records `sql_approved` AND closes the phase automatically (`kind:"phase"` only as fallback if it reports an error) |
|
||||||
@@ -60,8 +61,8 @@ kind:"cte_result"` of the plan (F6), `reviewer_confirm kind:"sql"` (F7), and
|
|||||||
with the deliberate `reviewer_confirm kind:"phase"` summary gate. The `advance:true`
|
with the deliberate `reviewer_confirm kind:"phase"` summary gate. The `advance:true`
|
||||||
flag on `reviewer_decide` is a shortcut that auto-advances ONLY F2 when the memory phase
|
flag on `reviewer_decide` is a shortcut that auto-advances ONLY F2 when the memory phase
|
||||||
recorded nothing and F6 when it is skipped/empty; everywhere else it is a silent no-op,
|
recorded nothing and F6 when it is skipped/empty; everywhere else it is a silent no-op,
|
||||||
so never rely on it to advance. The self-closing gates are the five listed above
|
so never rely on it to advance. The self-closing mechanisms are the five listed above
|
||||||
(F3 `rewrite_question`, F4 `reviewer_schema_linking` `advance:true`, F6 last
|
(F3 `rewrite_question`, F4 final schema persistence, F6 last
|
||||||
`kind:"cte_result"`, F7 `kind:"sql"`, F8 `reviewer_memory_promote`) — after one of
|
`kind:"cte_result"`, F7 `kind:"sql"`, F8 `reviewer_memory_promote`) — after one of
|
||||||
those, do NOT add a `reviewer_confirm kind:"phase"` that merely echoes it; the phase is
|
those, do NOT add a `reviewer_confirm kind:"phase"` that merely echoes it; the phase is
|
||||||
already closed.
|
already closed.
|
||||||
@@ -286,6 +287,9 @@ Prerequisite: Phase 3 closed.
|
|||||||
columns: project exactly those in the final SELECT (Phase 6/7); you remain free
|
columns: project exactly those in the final SELECT (Phase 6/7); you remain free
|
||||||
to reference other columns as join keys or filter predicates when the query
|
to reference other columns as join keys or filter predicates when the query
|
||||||
requires them.
|
requires them.
|
||||||
|
Call `reviewer_schema_linking` before the join review. For a multi-table plan,
|
||||||
|
`advance:true` will report that F4 remains open because the structured joins are
|
||||||
|
not present yet; this is expected. Stay in F4 and continue with the join-only gate.
|
||||||
Propose **all required joins together in a separate, join-only**
|
Propose **all required joins together in a separate, join-only**
|
||||||
`reviewer_decide(advance:false)`, registering `join_modified`. Do not mix
|
`reviewer_decide(advance:false)`, registering `join_modified`. Do not mix
|
||||||
`join_modified` with other decision types in that call. The gate renders this proposal
|
`join_modified` with other decision types in that call. The gate renders this proposal
|
||||||
@@ -317,7 +321,8 @@ Prerequisite: Phase 3 closed.
|
|||||||
excluded:[...], open_questions:[], concept_formulas:[]}` — `candidates`/`excluded`
|
excluded:[...], open_questions:[], concept_formulas:[]}` — `candidates`/`excluded`
|
||||||
are owned by `reviewer_schema_linking`/`sync-schema-linking` (step 2), so if you
|
are owned by `reviewer_schema_linking`/`sync-schema-linking` (step 2), so if you
|
||||||
call `write_schema_linking` after step 2, carry over its `candidates`/`excluded`
|
call `write_schema_linking` after step 2, carry over its `candidates`/`excluded`
|
||||||
unchanged rather than overwriting them. Then close with `reviewer_confirm
|
unchanged rather than overwriting them. After the reviewer-approved joins are present,
|
||||||
|
`write_schema_linking` closes F4 automatically. Do not add a `reviewer_confirm
|
||||||
kind:"phase"`. Do NOT run `tht session check` (that's Phase 5).
|
kind:"phase"`. Do NOT run `tht session check` (that's Phase 5).
|
||||||
|
|
||||||
## Phase 5 — Synthesis
|
## Phase 5 — Synthesis
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import os
|
||||||
|
from importlib import reload
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_default_config_honors_tht_config(monkeypatch):
|
||||||
|
from tht.cli import config_cmd
|
||||||
|
|
||||||
|
previous = os.environ.get("THT_CONFIG")
|
||||||
|
try:
|
||||||
|
monkeypatch.setenv("THT_CONFIG", "/app/harness/config/tht.yaml")
|
||||||
|
reload(config_cmd)
|
||||||
|
|
||||||
|
assert config_cmd.CONFIG_OPT.default == Path("/app/harness/config/tht.yaml")
|
||||||
|
finally:
|
||||||
|
if previous is None:
|
||||||
|
monkeypatch.delenv("THT_CONFIG", raising=False)
|
||||||
|
else:
|
||||||
|
monkeypatch.setenv("THT_CONFIG", previous)
|
||||||
|
reload(config_cmd)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from tht.decisions import append_decision
|
||||||
|
from tht.phase import advance_problems
|
||||||
|
|
||||||
|
|
||||||
|
def _phase_four_session(tmp_path):
|
||||||
|
session = tmp_path / "session"
|
||||||
|
session.mkdir()
|
||||||
|
for phase in range(1, 4):
|
||||||
|
append_decision(session, type="phase_approved", subject=f"phase:{phase}")
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase_four_cannot_close_multiple_tables_without_reviewed_structured_joins(tmp_path):
|
||||||
|
session = _phase_four_session(tmp_path)
|
||||||
|
(session / "schema_linking.json").write_text(json.dumps({
|
||||||
|
"question": "q",
|
||||||
|
"candidates": [
|
||||||
|
{"kind": "table", "name": "fact_event", "decision": "promoted"},
|
||||||
|
{"kind": "table", "name": "dim_patient", "decision": "promoted"},
|
||||||
|
],
|
||||||
|
"joins": [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
problems = advance_problems(session, 4)
|
||||||
|
|
||||||
|
assert any("join" in problem.lower() for problem in problems)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase_four_allows_a_single_promoted_table_without_joins(tmp_path):
|
||||||
|
session = _phase_four_session(tmp_path)
|
||||||
|
(session / "schema_linking.json").write_text(json.dumps({
|
||||||
|
"question": "q",
|
||||||
|
"candidates": [
|
||||||
|
{"kind": "table", "name": "dim_patient", "decision": "promoted"},
|
||||||
|
],
|
||||||
|
"joins": [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
assert advance_problems(session, 4) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase_four_allows_reviewed_structured_joins_for_multiple_tables(tmp_path):
|
||||||
|
session = _phase_four_session(tmp_path)
|
||||||
|
append_decision(
|
||||||
|
session,
|
||||||
|
type="join_modified",
|
||||||
|
subject="fact_event_to_patient",
|
||||||
|
detail="fact_event.cod_paz = dim_patient.cod_paz",
|
||||||
|
)
|
||||||
|
(session / "schema_linking.json").write_text(json.dumps({
|
||||||
|
"question": "q",
|
||||||
|
"candidates": [
|
||||||
|
{"kind": "table", "name": "fact_event", "decision": "promoted"},
|
||||||
|
{"kind": "table", "name": "dim_patient", "decision": "promoted"},
|
||||||
|
],
|
||||||
|
"joins": [
|
||||||
|
{"from": "fact_event.cod_paz", "to": "dim_patient.cod_paz"},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
assert advance_problems(session, 4) == []
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class ComposeLoader(yaml.SafeLoader):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _compose_override(loader, node):
|
||||||
|
if isinstance(node, yaml.MappingNode):
|
||||||
|
return loader.construct_mapping(node)
|
||||||
|
return loader.construct_sequence(node)
|
||||||
|
|
||||||
|
|
||||||
|
ComposeLoader.add_constructor("!override", _compose_override)
|
||||||
|
|
||||||
|
|
||||||
|
def test_psd_overlay_uses_generated_workspace_for_default_and_named_commands():
|
||||||
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
compose = yaml.load(
|
||||||
|
(root / "deploy/compose.psd-local.yaml.example").read_text(),
|
||||||
|
Loader=ComposeLoader,
|
||||||
|
)
|
||||||
|
core = compose["services"]["core"]
|
||||||
|
|
||||||
|
assert core["environment"]["THT_CONFIG"] == "/app/harness/config/tht.yaml"
|
||||||
|
assert core["environment"]["THT_SECRETS_FILE"] == "/run/secrets/thothii.secrets"
|
||||||
|
for name in (
|
||||||
|
"THT_DB_NAME", "THT_DWH_REST_URL", "THT_VEC_REST_URL",
|
||||||
|
"THT_VEC_WRITE_REST_URL", "THT_OLLAMA_URL", "THT_PROFILE",
|
||||||
|
"PI_PROVIDER", "PI_MODEL", "PI_THINKING",
|
||||||
|
):
|
||||||
|
assert name in core["environment"]
|
||||||
|
def target(volume):
|
||||||
|
if isinstance(volume, dict):
|
||||||
|
return volume["target"]
|
||||||
|
parts = volume.rsplit(":", 2)
|
||||||
|
return parts[-2] if parts[-1] in {"ro", "rw"} else parts[-1]
|
||||||
|
|
||||||
|
targets = {target(volume) for volume in core["volumes"]}
|
||||||
|
assert "/app/harness/config/tht.yaml" in targets
|
||||||
|
assert "/app/harness/workspaces/psd.yaml" not in targets
|
||||||
|
assert "/data/workspaces/psd/config/tht.yaml" not in targets
|
||||||
|
assert "/data" in targets
|
||||||
|
assert "/home/thoth/.pi" in targets
|
||||||
|
assert "/home/thoth/.pi/agent/models.json" in targets
|
||||||
|
assert "/home/thoth/.pi/agent/settings.json" in targets
|
||||||
|
assert "/data/evidence" in targets
|
||||||
|
assert "/run/secrets/thothii.secrets" in targets
|
||||||
|
assert set(compose["volumes"]) == {"thoth_data", "thoth_pi_config"}
|
||||||
|
assert core["networks"]["default"]["aliases"] == ["core", "thothii-core"]
|
||||||
|
assert compose["networks"]["default"] == {
|
||||||
|
"external": True,
|
||||||
|
"name": "thothii_default",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_image_prepares_the_writable_pi_profile_before_mounting_config_files():
|
||||||
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
dockerfile = (root / "docker/core.Dockerfile").read_text()
|
||||||
|
|
||||||
|
assert "mkdir -p /home/thoth/.pi/agent" in dockerfile
|
||||||
|
assert "chown -R thoth:thoth /home/thoth/.pi" in dockerfile
|
||||||
|
assert (
|
||||||
|
"cp --remove-destination /app/harness/workspaces/local.yaml "
|
||||||
|
"/app/harness/config/tht.yaml" in dockerfile
|
||||||
|
)
|
||||||
|
assert "ln -sf /app/harness/workspaces/local.yaml /app/harness/config/tht.yaml" not in dockerfile
|
||||||
|
assert (
|
||||||
|
"ln -sfn /app/harness/config/tht.yaml /app/harness/workspaces/psd.yaml"
|
||||||
|
in dockerfile
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_psd_bootstrap_materializes_the_base_compose_env_file(tmp_path):
|
||||||
|
source_root = Path(__file__).resolve().parents[2]
|
||||||
|
root = tmp_path / "ThothII"
|
||||||
|
(root / "scripts").mkdir(parents=True)
|
||||||
|
(root / "deploy/workspaces").mkdir(parents=True)
|
||||||
|
shutil.copy(
|
||||||
|
source_root / "scripts/bootstrap-local-psd-docker-config.sh",
|
||||||
|
root / "scripts/bootstrap-local-psd-docker-config.sh",
|
||||||
|
)
|
||||||
|
shutil.copy(
|
||||||
|
source_root / "deploy/compose.psd-local.yaml.example",
|
||||||
|
root / "deploy/compose.psd-local.yaml.example",
|
||||||
|
)
|
||||||
|
shutil.copy(
|
||||||
|
source_root / "deploy/workspaces/psd.yaml.example",
|
||||||
|
root / "deploy/workspaces/psd.yaml.example",
|
||||||
|
)
|
||||||
|
source_env = tmp_path / "source.env"
|
||||||
|
source_env.write_text("\n".join([
|
||||||
|
"THT_DB_NAME=postgres",
|
||||||
|
"THT_DWH_REST_URL=https://dwh.invalid/",
|
||||||
|
"THT_VEC_REST_URL=https://vec.invalid/read/",
|
||||||
|
"THT_VEC_WRITE_REST_URL=https://vec.invalid/write/",
|
||||||
|
"THT_DWH_API_KEY=dwh",
|
||||||
|
"THT_VEC_API_KEY=reader",
|
||||||
|
"THT_VEC_WRITE_API_KEY=writer",
|
||||||
|
"",
|
||||||
|
]))
|
||||||
|
workspace = tmp_path / "workspace"
|
||||||
|
workspace.mkdir()
|
||||||
|
auth = tmp_path / "auth.json"
|
||||||
|
auth.write_text('{"zai":{"key":"model"}}')
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"sh", str(root / "scripts/bootstrap-local-psd-docker-config.sh"),
|
||||||
|
str(source_env), str(workspace), str(auth),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (root / "deploy/thothii.env").is_file()
|
||||||
|
assert "THT_SECRETS_FILE=./deploy/secrets/thothii.secrets" in (
|
||||||
|
root / ".env"
|
||||||
|
).read_text()
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
@@ -7,7 +8,10 @@ from tht.config import ConfigError, load_config
|
|||||||
config_app = typer.Typer(help="Gestione configurazione")
|
config_app = typer.Typer(help="Gestione configurazione")
|
||||||
|
|
||||||
CONFIG_OPT = typer.Option(
|
CONFIG_OPT = typer.Option(
|
||||||
Path("config/tht.yaml"), "--config", "-c", help="Percorso del file di configurazione."
|
Path(os.environ.get("THT_CONFIG", "config/tht.yaml")),
|
||||||
|
"--config",
|
||||||
|
"-c",
|
||||||
|
help="Percorso del file di configurazione.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,28 @@ def advance_problems(source: Path | SessionSnapshot, phase: int) -> list[str]:
|
|||||||
l'evaluator generico (F2 pieno) entra in un secondo momento.
|
l'evaluator generico (F2 pieno) entra in un secondo momento.
|
||||||
"""
|
"""
|
||||||
problems: list[str] = []
|
problems: list[str] = []
|
||||||
|
if phase == 4:
|
||||||
|
raw = _artifact(source, "schema_linking", "schema_linking.json")
|
||||||
|
if raw is None:
|
||||||
|
problems.append("schema_linking.json assente (Fase 4)")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
linking = SchemaLinking.model_validate(json.loads(raw))
|
||||||
|
except (json.JSONDecodeError, ValidationError) as e:
|
||||||
|
problems.append(f"schema_linking.json non valido (Fase 4): {e}")
|
||||||
|
else:
|
||||||
|
promoted_tables = {
|
||||||
|
candidate.name
|
||||||
|
for candidate in linking.candidates
|
||||||
|
if candidate.kind == "table" and candidate.decision == "promoted"
|
||||||
|
}
|
||||||
|
if len(promoted_tables) > 1 and (
|
||||||
|
not linking.joins or not _has_decision(source, "join_modified")
|
||||||
|
):
|
||||||
|
problems.append(
|
||||||
|
"Fase 4: più tabelle promosse richiedono join strutturati in "
|
||||||
|
"schema_linking.json e una decisione join_modified del reviewer"
|
||||||
|
)
|
||||||
if phase == 3 and not _has_decision(source, "question_rewritten"):
|
if phase == 3 and not _has_decision(source, "question_rewritten"):
|
||||||
problems.append("manca la decisione question_rewritten (Fase 3)")
|
problems.append("manca la decisione question_rewritten (Fase 3)")
|
||||||
if phase == 5:
|
if phase == 5:
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ test -n "$model_key"
|
|||||||
|
|
||||||
umask 077
|
umask 077
|
||||||
mkdir -p "$root/deploy/secrets" "$root/deploy/workspaces"
|
mkdir -p "$root/deploy/secrets" "$root/deploy/workspaces"
|
||||||
|
: >"$root/deploy/thothii.env"
|
||||||
cp "$root/deploy/compose.psd-local.yaml.example" "$root/deploy/compose.psd-local.yaml"
|
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"
|
cp "$root/deploy/workspaces/psd.yaml.example" "$root/deploy/workspaces/psd.yaml"
|
||||||
cat >"$root/.env" <<EOF
|
cat >"$root/.env" <<EOF
|
||||||
COMPOSE_FILE=compose.yaml:deploy/compose.psd-local.yaml
|
COMPOSE_FILE=compose.yaml:deploy/compose.psd-local.yaml
|
||||||
COMPOSE_PROFILES=
|
COMPOSE_PROFILES=
|
||||||
THT_SECRETS_FILE=deploy/secrets/thothii.secrets
|
THT_SECRETS_FILE=./deploy/secrets/thothii.secrets
|
||||||
THOTH_HTTP_PORT=8080
|
THOTH_HTTP_PORT=8080
|
||||||
AUTH_MODE=none
|
AUTH_MODE=none
|
||||||
THOTH_PUBLIC_EXPOSURE=false
|
THOTH_PUBLIC_EXPOSURE=false
|
||||||
@@ -64,5 +65,5 @@ EOF
|
|||||||
else
|
else
|
||||||
printf '%s\n' 'THT_CA=/etc/ssl/certs/ca-certificates.crt' >>"$root/deploy/secrets/thothii.secrets"
|
printf '%s\n' 'THT_CA=/etc/ssl/certs/ca-certificates.crt' >>"$root/deploy/secrets/thothii.secrets"
|
||||||
fi
|
fi
|
||||||
chmod 600 "$root/.env" "$root/deploy/secrets/thothii.secrets"
|
chmod 600 "$root/.env" "$root/deploy/thothii.env" "$root/deploy/secrets/thothii.secrets"
|
||||||
echo "Local PSD Docker configuration materialized without printing secret values."
|
echo "Local PSD Docker configuration materialized without printing secret values."
|
||||||
|
|||||||
Reference in New Issue
Block a user