@tea-agent/loop-agent 0.28.12 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +61 -15
- package/dist/commands/client-recovery.js +56 -1
- package/dist/commands/init-upgrade.js +186 -21
- package/dist/executors/dag-pi-executor.js +49 -2
- package/dist/executors/shell-executor.js +135 -0
- package/dist/worker/console/app-data.js +132 -11
- package/dist/worker/console/chat/pi-runtime.js +24 -42
- package/dist/worker/console/chat/resource-loader.js +11 -20
- package/dist/worker/console/chat/routes.js +7 -8
- package/dist/worker/console/chat/runtime-context.js +1 -1
- package/dist/worker/console/chat/tools.js +67 -54
- package/dist/worker/console/operation-runner.js +15 -1
- package/dist/worker/console/operation-store.js +70 -49
- package/dist/worker/console/operator-actions.js +57 -1
- package/dist/worker/console/static/assets/index-Cwx-ZVEQ.js +29 -0
- package/dist/worker/console/static/favicon.svg +37 -0
- package/dist/worker/console/static/index.html +2 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +846 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +418 -0
- package/dist/workflows/dag/init-hybrid.js +20 -6
- package/dist/workflows/dag/node-execution.js +31 -2
- package/dist/workflows/dag/retry-policy.js +55 -18
- package/dist/workflows/dag/types.js +1 -0
- package/dist/workflows/dag/validate.js +38 -3
- package/docs/operations/README.md +1 -1
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +3 -3
- package/docs/templates/backend-test-dag.json +36 -11
- package/docs/templates/init-managed-agents.md +1 -1
- package/harness.json +2 -2
- package/package.json +2 -1
- package/dist/worker/console/static/assets/index-BfRgtLF4.js +0 -29
|
@@ -30,6 +30,7 @@ import { materializeBackendTestExecutionContract } from "../workflows/dag/backen
|
|
|
30
30
|
import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
|
|
31
31
|
import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
|
|
32
32
|
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
33
|
+
import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
|
|
33
34
|
import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assertBackendPytestCollectionFresh, buildBackendPytestAssetInventory, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
|
|
34
35
|
import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
35
36
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
@@ -768,6 +769,77 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
768
769
|
durationMs: Date.now() - started,
|
|
769
770
|
};
|
|
770
771
|
}
|
|
772
|
+
// Scenario-param consistency (P1): assess -> deterministic repair <=1 -> reassess final.
|
|
773
|
+
try {
|
|
774
|
+
const initialAssessment = await assessBackendScenarioParamConsistency({
|
|
775
|
+
workspaceRoot: input.cwd,
|
|
776
|
+
phase: "initial",
|
|
777
|
+
repairAttempt: 0,
|
|
778
|
+
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
779
|
+
});
|
|
780
|
+
const initialArtifacts = await writeBackendScenarioParamArtifacts({
|
|
781
|
+
runDir: meta.runDir,
|
|
782
|
+
facts: initialAssessment.facts,
|
|
783
|
+
markdown: initialAssessment.markdown,
|
|
784
|
+
});
|
|
785
|
+
outputs.push(`scenarioParamInitial=${initialArtifacts.reportPath}`, `scenarioParamInitialFacts=${initialArtifacts.factsPath}`);
|
|
786
|
+
let finalFacts = initialAssessment.facts;
|
|
787
|
+
let finalMarkdown = initialAssessment.markdown;
|
|
788
|
+
if (initialAssessment.facts.repairEligible) {
|
|
789
|
+
const repair = await applyDeterministicScenarioParamRepairs({
|
|
790
|
+
workspaceRoot: input.cwd,
|
|
791
|
+
facts: initialAssessment.facts,
|
|
792
|
+
});
|
|
793
|
+
const auditPath = await writeScenarioParamRepairAudit({
|
|
794
|
+
runDir: meta.runDir,
|
|
795
|
+
audit: repair.audit,
|
|
796
|
+
changedFiles: repair.changedFiles,
|
|
797
|
+
});
|
|
798
|
+
outputs.push(`scenarioParamRepairAudit=${auditPath}`, `scenarioParamRepaired=${repair.repaired.join(",") || "(none)"}`);
|
|
799
|
+
const reassessment = await assessBackendScenarioParamConsistency({
|
|
800
|
+
workspaceRoot: input.cwd,
|
|
801
|
+
phase: "final",
|
|
802
|
+
repairAttempt: 1,
|
|
803
|
+
strictScenarioParamGate: initialAssessment.facts.strictScenarioParamGate,
|
|
804
|
+
});
|
|
805
|
+
finalFacts = reassessment.facts;
|
|
806
|
+
finalMarkdown = reassessment.markdown;
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
finalFacts = {
|
|
810
|
+
...initialAssessment.facts,
|
|
811
|
+
phase: "final",
|
|
812
|
+
repairAttempt: 0,
|
|
813
|
+
repairEligible: false,
|
|
814
|
+
};
|
|
815
|
+
finalMarkdown = initialAssessment.markdown.replace("Phase: initial", "Phase: final");
|
|
816
|
+
}
|
|
817
|
+
const finalArtifacts = await writeBackendScenarioParamArtifacts({
|
|
818
|
+
runDir: meta.runDir,
|
|
819
|
+
facts: finalFacts,
|
|
820
|
+
markdown: finalMarkdown,
|
|
821
|
+
});
|
|
822
|
+
outputs.push(`scenarioParamFinal=${finalArtifacts.reportPath}`, `scenarioParamFinalFacts=${finalArtifacts.factsPath}`, `scenarioParamMismatch=${finalFacts.summary.mismatchCount}`, finalMarkdown);
|
|
823
|
+
if (finalFacts.strictScenarioParamGate &&
|
|
824
|
+
finalFacts.summary.mismatchCount > 0) {
|
|
825
|
+
return {
|
|
826
|
+
ok: false,
|
|
827
|
+
stdout: outputs.join("\n\n"),
|
|
828
|
+
stderr: "backend-test strict scenario-param gate blocked residual MISMATCH before execute",
|
|
829
|
+
failureCategory: "invalid-output",
|
|
830
|
+
durationMs: Date.now() - started,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
836
|
+
const report = "# Backend Test Scenario-Param Consistency\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Scenario-param analysis crashed: " +
|
|
837
|
+
message +
|
|
838
|
+
"\n";
|
|
839
|
+
const reportPath = await writeRunReport(meta.runDir, "backend-test-scenario-param-consistency.md", report);
|
|
840
|
+
outputs.push(`scenarioParam=${reportPath}`, report);
|
|
841
|
+
// Advisory by default: do not fail the node on scenario-param infrastructure errors.
|
|
842
|
+
}
|
|
771
843
|
}
|
|
772
844
|
else if (pipeline === "markdown-manifest") {
|
|
773
845
|
const sourceBinding = meta.spec.sourceBinding;
|
|
@@ -989,6 +1061,69 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
989
1061
|
});
|
|
990
1062
|
const markdownPath = await writeRunReport(meta.runDir, "backend-test.md", facts);
|
|
991
1063
|
const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
|
|
1064
|
+
// Structured failure analysis report (P2); emitted even when there are zero failures.
|
|
1065
|
+
let scenarioParamFinalStatus = "UNAVAILABLE";
|
|
1066
|
+
let scenarioParamRepairAttempt = 0;
|
|
1067
|
+
const scenarioParamStatusByToken = new Map();
|
|
1068
|
+
try {
|
|
1069
|
+
const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
|
|
1070
|
+
scenarioParamFinalStatus =
|
|
1071
|
+
scenarioFacts.summary.mismatchCount > 0 ? "FAIL" : "PASS";
|
|
1072
|
+
scenarioParamRepairAttempt = scenarioFacts.repairAttempt;
|
|
1073
|
+
for (const entry of scenarioFacts.entries) {
|
|
1074
|
+
scenarioParamStatusByToken.set(entry.caseId, entry.status);
|
|
1075
|
+
scenarioParamStatusByToken.set(entry.tpId, entry.status);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
catch {
|
|
1079
|
+
// optional facts
|
|
1080
|
+
}
|
|
1081
|
+
const failureCases = parsed.cases.filter((item) => item.status === "failure" || item.status === "error");
|
|
1082
|
+
const failureAnalysisMarkdown = renderBackendTestFailureAnalysis({
|
|
1083
|
+
generatedAt: new Date().toISOString(),
|
|
1084
|
+
htmlReportPath: "reports/backend-test.html",
|
|
1085
|
+
total: parsed.tests,
|
|
1086
|
+
passed: parsed.passed,
|
|
1087
|
+
failed: parsed.failed + parsed.errors,
|
|
1088
|
+
durationLabel: `${parsed.durationMs ?? 0}ms`,
|
|
1089
|
+
environmentSummary: await readFile(path.join(reportsDir, "backend-test-environment.md"), "utf8").catch(() => "unavailable"),
|
|
1090
|
+
scenarioParamFinalStatus,
|
|
1091
|
+
repairAttempt: scenarioParamRepairAttempt,
|
|
1092
|
+
failures: failureCases.map((result) => {
|
|
1093
|
+
const caseMatch = result.name.match(/\bBE-[A-Z0-9_-]+-\d{2,3}\b/);
|
|
1094
|
+
const tpMatch = result.name.match(/\bTP-[A-Z0-9-]+\b/);
|
|
1095
|
+
const caseId = caseMatch?.[0] ?? result.name;
|
|
1096
|
+
const scenarioParamStatus = (tpMatch && scenarioParamStatusByToken.get(tpMatch[0])) ||
|
|
1097
|
+
scenarioParamStatusByToken.get(caseId);
|
|
1098
|
+
const evidence = [result.message, result.details]
|
|
1099
|
+
.filter(Boolean)
|
|
1100
|
+
.join("\n");
|
|
1101
|
+
const statusMatch = /assert\s+(\d{3})\s*==\s*(\d{3})/i.exec(evidence) ||
|
|
1102
|
+
/expected[^\d]*(\d{3})[\s\S]{0,40}actual[^\d]*(\d{3})/i.exec(evidence);
|
|
1103
|
+
return {
|
|
1104
|
+
name: result.name,
|
|
1105
|
+
caseId,
|
|
1106
|
+
scenario: cases.find((item) => item.id === caseId)?.scenario ??
|
|
1107
|
+
cases.find((item) => item.id === caseId)?.title ??
|
|
1108
|
+
result.name,
|
|
1109
|
+
expectedCode: statusMatch?.[2] ?? statusMatch?.[1],
|
|
1110
|
+
actualCode: statusMatch?.[1] ?? statusMatch?.[2],
|
|
1111
|
+
message: result.message || result.status,
|
|
1112
|
+
scriptPath: result.classname,
|
|
1113
|
+
durationLabel: result.durationMs !== undefined
|
|
1114
|
+
? `${result.durationMs}ms`
|
|
1115
|
+
: undefined,
|
|
1116
|
+
scenarioParamStatus,
|
|
1117
|
+
classification: classifyBackendTestFailureWithScenarioParam({
|
|
1118
|
+
message: result.message || "",
|
|
1119
|
+
details: result.details,
|
|
1120
|
+
scenarioParamStatus,
|
|
1121
|
+
}),
|
|
1122
|
+
};
|
|
1123
|
+
}),
|
|
1124
|
+
});
|
|
1125
|
+
const failureAnalysisPath = await writeRunReport(meta.runDir, "backend-test-failure-analysis.md", failureAnalysisMarkdown);
|
|
1126
|
+
outputs.push(`failureAnalysis=${failureAnalysisPath}`);
|
|
992
1127
|
// Deterministic L-5 dashboard: machine-computed metrics (not Pi-generated).
|
|
993
1128
|
// manifest is produced by an upstream finalize node; tolerate its absence
|
|
994
1129
|
// so a minimal DAG without manifest still gets a degraded dashboard.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, renameSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -100,6 +100,98 @@ export function assertSafeAppDataPath(baseDir, candidate) {
|
|
|
100
100
|
}
|
|
101
101
|
return resolved;
|
|
102
102
|
}
|
|
103
|
+
const WINDOWS_REPLACE_RETRY_DELAYS_MS = [10, 25, 50, 100, 200, 400];
|
|
104
|
+
const WINDOWS_REPLACE_RETRY_CODES = new Set([
|
|
105
|
+
"EACCES",
|
|
106
|
+
"EBUSY",
|
|
107
|
+
"EEXIST",
|
|
108
|
+
"EPERM",
|
|
109
|
+
]);
|
|
110
|
+
/**
|
|
111
|
+
* Atomically replace `resolved` with `tmp`.
|
|
112
|
+
* On Windows, destination locks (AV / indexer / concurrent writers) often yield
|
|
113
|
+
* EPERM/EEXIST on rename — retry, then fall back to unlink+rename.
|
|
114
|
+
*/
|
|
115
|
+
export async function replaceFileWithRetry(tmp, resolved, deps = {}) {
|
|
116
|
+
const doRename = deps.rename ?? rename;
|
|
117
|
+
const doRm = deps.rm ?? rm;
|
|
118
|
+
const platform = deps.platform ?? process.platform;
|
|
119
|
+
const sleep = deps.sleep ??
|
|
120
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
121
|
+
let lastError;
|
|
122
|
+
for (let attempt = 0;; attempt += 1) {
|
|
123
|
+
try {
|
|
124
|
+
await doRename(tmp, resolved);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
lastError = error;
|
|
129
|
+
const code = error.code ?? "";
|
|
130
|
+
const delay = WINDOWS_REPLACE_RETRY_DELAYS_MS[attempt];
|
|
131
|
+
if (platform !== "win32" ||
|
|
132
|
+
!WINDOWS_REPLACE_RETRY_CODES.has(code) ||
|
|
133
|
+
delay === undefined) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
await sleep(delay);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (platform === "win32") {
|
|
140
|
+
const code = lastError?.code ?? "";
|
|
141
|
+
if (WINDOWS_REPLACE_RETRY_CODES.has(code)) {
|
|
142
|
+
try {
|
|
143
|
+
await doRm(resolved, { force: true });
|
|
144
|
+
await doRename(tmp, resolved);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
catch (fallbackError) {
|
|
148
|
+
throw lastError ?? fallbackError;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw lastError instanceof Error
|
|
153
|
+
? lastError
|
|
154
|
+
: new Error(String(lastError ?? "replaceFileWithRetry failed"));
|
|
155
|
+
}
|
|
156
|
+
function replaceFileWithRetrySync(tmp, resolved) {
|
|
157
|
+
let lastError;
|
|
158
|
+
for (let attempt = 0;; attempt += 1) {
|
|
159
|
+
try {
|
|
160
|
+
renameSync(tmp, resolved);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
lastError = error;
|
|
165
|
+
const code = error.code ?? "";
|
|
166
|
+
const delay = WINDOWS_REPLACE_RETRY_DELAYS_MS[attempt];
|
|
167
|
+
if (process.platform !== "win32" ||
|
|
168
|
+
!WINDOWS_REPLACE_RETRY_CODES.has(code) ||
|
|
169
|
+
delay === undefined) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
const end = Date.now() + delay;
|
|
173
|
+
while (Date.now() < end) {
|
|
174
|
+
/* busy-wait: sync path used only for small boot/SSE writes */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (process.platform === "win32") {
|
|
179
|
+
const code = lastError?.code ?? "";
|
|
180
|
+
if (WINDOWS_REPLACE_RETRY_CODES.has(code)) {
|
|
181
|
+
try {
|
|
182
|
+
rmSync(resolved, { force: true });
|
|
183
|
+
renameSync(tmp, resolved);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
catch (fallbackError) {
|
|
187
|
+
throw lastError ?? fallbackError;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
throw lastError instanceof Error
|
|
192
|
+
? lastError
|
|
193
|
+
: new Error(String(lastError ?? "replaceFileWithRetrySync failed"));
|
|
194
|
+
}
|
|
103
195
|
export async function writeSecureJson(filePath, value) {
|
|
104
196
|
const dir = path.dirname(filePath);
|
|
105
197
|
ensureSecureDir(dir);
|
|
@@ -108,12 +200,18 @@ export async function writeSecureJson(filePath, value) {
|
|
|
108
200
|
const body = `${JSON.stringify(value, null, 2)}\n`;
|
|
109
201
|
await writeFile(tmp, body, { encoding: "utf8", mode: 0o600 });
|
|
110
202
|
try {
|
|
111
|
-
|
|
203
|
+
try {
|
|
204
|
+
chmodSync(tmp, 0o600);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// best-effort
|
|
208
|
+
}
|
|
209
|
+
await replaceFileWithRetry(tmp, resolved);
|
|
112
210
|
}
|
|
113
|
-
catch {
|
|
114
|
-
|
|
211
|
+
catch (error) {
|
|
212
|
+
await rm(tmp, { force: true }).catch(() => undefined);
|
|
213
|
+
throw error;
|
|
115
214
|
}
|
|
116
|
-
await rename(tmp, resolved);
|
|
117
215
|
try {
|
|
118
216
|
chmodSync(resolved, 0o600);
|
|
119
217
|
}
|
|
@@ -182,10 +280,33 @@ export function writeSecureJsonSync(filePath, value) {
|
|
|
182
280
|
const dir = path.dirname(filePath);
|
|
183
281
|
ensureSecureDir(dir);
|
|
184
282
|
const resolved = assertSafeAppDataPath(dir, filePath);
|
|
185
|
-
const tmp = `${resolved}.${process.pid}.tmp`;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
283
|
+
const tmp = `${resolved}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
284
|
+
try {
|
|
285
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, {
|
|
286
|
+
encoding: "utf8",
|
|
287
|
+
mode: 0o600,
|
|
288
|
+
});
|
|
289
|
+
try {
|
|
290
|
+
chmodSync(tmp, 0o600);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// best-effort
|
|
294
|
+
}
|
|
295
|
+
replaceFileWithRetrySync(tmp, resolved);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
try {
|
|
299
|
+
rmSync(tmp, { force: true });
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// best-effort cleanup
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
chmodSync(resolved, 0o600);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// best-effort
|
|
311
|
+
}
|
|
191
312
|
}
|
|
@@ -13,11 +13,10 @@
|
|
|
13
13
|
* (noContextFiles / closed surface); credential/model plane is shared via
|
|
14
14
|
* the same agentDir auth.json/models.json.
|
|
15
15
|
* - Active tools are pinned to the operator-chat surface at session create
|
|
16
|
-
* AND re-pinned before each prompt (three-gate, design §7.5):
|
|
17
|
-
* operator action set
|
|
18
|
-
* read/
|
|
19
|
-
* (
|
|
20
|
-
* the SDK registry and can never be activated (2026-07-25 widening).
|
|
16
|
+
* AND re-pinned before each prompt (three-gate, design §7.5 / ADR 0011):
|
|
17
|
+
* the FULL operator action set PLUS full Pi builtins
|
|
18
|
+
* (read/write/edit/bash/grep/find/ls) and optional safe-*. Non-Pi write
|
|
19
|
+
* channels (apply_patch/full-tools/shell/coding-chat) stay excluded.
|
|
21
20
|
*
|
|
22
21
|
* The actual SDK calls are injected via `PiSdkBindings` so this module is
|
|
23
22
|
* unit-testable without a live Pi install. Production bindings come from
|
|
@@ -49,35 +48,34 @@ import { filterActiveInterviewTools } from "../interview/tools.js";
|
|
|
49
48
|
*/
|
|
50
49
|
const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
51
50
|
"You are the General Operator Chat for loop-agent / agent-worker.",
|
|
52
|
-
"You are an OPERATOR:
|
|
53
|
-
"
|
|
54
|
-
"safe-read / safe-grep enforce a repo-relative path boundary, a sensitive-file denylist (.env* / *.key / *.pem / .git/** / auth.json / sessions/**) and secret scrubbing.
|
|
55
|
-
"You do NOT have
|
|
56
|
-
"High-risk mutations
|
|
51
|
+
"You are an OPERATOR first: orchestrate and inspect via operator_* tools, and you also have full Pi repository tools (read, write, edit, bash, grep, find, ls) plus optional safe-read/safe-grep/git-status/git-diff.",
|
|
52
|
+
"Capability honesty (ADR 0011): read/write/edit/bash ARE available. Prefer governed loop-agent / Agent DAG paths (implement-pi / repair-pi) and Human Gate for large refactors, public contracts, credentials, or production-risk changes — treat direct write/edit/bash as soft-disciplined, not as a second DAG kernel.",
|
|
53
|
+
"safe-read / safe-grep enforce a repo-relative path boundary, a sensitive-file denylist (.env* / *.key / *.pem / .git/** / auth.json / sessions/**) and secret scrubbing. Prefer them for sensitive probes; avoid dumping secrets via raw read/bash.",
|
|
54
|
+
"You do NOT have apply_patch / full-tools / shell / coding-chat as alternate coding runtimes — those non-Pi channels stay denied.",
|
|
55
|
+
"High-risk mutations still use prepare + browser Human Gate. Prepare contract via interview then apply via contractApply, prepare DAG runs via prepareDagConfirmation, prepare other mutations via prepareMutationGate, then confirm in the browser Human Gate. When a high-risk action (contractApply / runDag / dagRerun / etc.) fails on missing prepared state, diagnose with status/doctor/dagReport/inspect/contractShow/read/safe-read/safe-grep.",
|
|
57
56
|
"You cannot self-confirm a DAG run: confirmDagConfirmation is a human-only action (executed by the browser with a server-signed confirmation token). You may only prepare it via prepareDagConfirmation; the user must confirm in the UI.",
|
|
58
|
-
"Prefer read-only
|
|
57
|
+
"Prefer read-only diagnosis (status, doctor, dagReport, inspect, contractShow, read, safe-read, safe-grep, git-status) before mutating.",
|
|
59
58
|
].join("\n");
|
|
60
59
|
/**
|
|
61
|
-
* Built-in SDK
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* `echo >` / `tee` / `sed -i` / `node -e writeFile`, bypassing the DAG write
|
|
65
|
-
* boundary. The residual read-only repo probing is served by the custom
|
|
66
|
-
* safe-read / safe-grep / find / ls tools (see buildExploreCustomTools) which
|
|
67
|
-
* enforce a repo-relative path boundary, a sensitive-file denylist, and
|
|
68
|
-
* secret scrubbing on the returned content. edit/write/... stay excluded.
|
|
60
|
+
* Built-in SDK tool names registered for every Chat session (ADR 0011).
|
|
61
|
+
* Full Pi builtins: read/write/edit/bash/grep/find/ls. safe-* custom tools
|
|
62
|
+
* remain available in parallel for bounded sensitive probes.
|
|
69
63
|
*/
|
|
70
64
|
export const OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS = Object.freeze([
|
|
65
|
+
"read",
|
|
66
|
+
"write",
|
|
67
|
+
"edit",
|
|
68
|
+
"bash",
|
|
69
|
+
"grep",
|
|
71
70
|
"find",
|
|
72
71
|
"ls",
|
|
73
72
|
]);
|
|
74
73
|
/**
|
|
75
74
|
* Canonical active tool-set handed to setActiveToolsByName at every gate:
|
|
76
|
-
* every model-callable operator action (dynamic, from the registry) PLUS
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* excluded at the registry level and thus cannot be activated.
|
|
75
|
+
* every model-callable operator action (dynamic, from the registry) PLUS full
|
|
76
|
+
* Pi builtins (ADR 0011) PLUS safe explore custom tools (safe-read /
|
|
77
|
+
* safe-grep / git-status / git-diff). Non-Pi write channels (apply_patch /
|
|
78
|
+
* full-tools / shell / coding-chat) stay excluded.
|
|
81
79
|
*/
|
|
82
80
|
export const OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS = Object.freeze([
|
|
83
81
|
"safe-read",
|
|
@@ -549,13 +547,10 @@ export class ConsolePiRuntime {
|
|
|
549
547
|
const { session } = await this.bindings.createSessionFromServices({
|
|
550
548
|
services,
|
|
551
549
|
sessionManager,
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
// grep/bash are replaced by the safe customTools (M0-A).
|
|
550
|
+
// Gate 1 (ADR 0011): full Pi builtins registered; only non-Pi write
|
|
551
|
+
// channels excluded (apply_patch / full-tools / shell / coding-chat).
|
|
555
552
|
tools: [...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS],
|
|
556
553
|
excludeTools: [
|
|
557
|
-
"edit",
|
|
558
|
-
"write",
|
|
559
554
|
"apply_patch",
|
|
560
555
|
"apply-patch",
|
|
561
556
|
"full-tools",
|
|
@@ -563,14 +558,6 @@ export class ConsolePiRuntime {
|
|
|
563
558
|
"coding-chat",
|
|
564
559
|
"coding_chat",
|
|
565
560
|
"shell",
|
|
566
|
-
// M0-A: bash is removed entirely (write-via-redirect escape). The
|
|
567
|
-
// model gets safe-read / safe-grep custom tools instead.
|
|
568
|
-
"bash",
|
|
569
|
-
// M0-A: built-in read/grep are replaced by custom safe-read/safe-grep
|
|
570
|
-
// that enforce a repo-relative boundary, a sensitive-file denylist,
|
|
571
|
-
// and secret scrubbing (roadmap G16).
|
|
572
|
-
"read",
|
|
573
|
-
"grep",
|
|
574
561
|
],
|
|
575
562
|
...(customTools && customTools.length > 0
|
|
576
563
|
? { customTools }
|
|
@@ -836,8 +823,6 @@ export class ConsolePiRuntime {
|
|
|
836
823
|
sessionManager: init.sessionManager,
|
|
837
824
|
tools: [...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS],
|
|
838
825
|
excludeTools: [
|
|
839
|
-
"edit",
|
|
840
|
-
"write",
|
|
841
826
|
"apply_patch",
|
|
842
827
|
"apply-patch",
|
|
843
828
|
"full-tools",
|
|
@@ -845,9 +830,6 @@ export class ConsolePiRuntime {
|
|
|
845
830
|
"coding-chat",
|
|
846
831
|
"coding_chat",
|
|
847
832
|
"shell",
|
|
848
|
-
"bash",
|
|
849
|
-
"read",
|
|
850
|
-
"grep",
|
|
851
833
|
],
|
|
852
834
|
...(customTools && customTools.length > 0 ? { customTools } : {}),
|
|
853
835
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Operator Chat —
|
|
2
|
+
* Operator Chat — ResourceLoader contract (ADR 0011 full Pi tools).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* are file-WRITING coding tools (edit/write/apply_patch/full-tools/shell/
|
|
10
|
-
* coding-chat) AND bash/read/grep (replaced by the safe versions). hasBash is
|
|
11
|
-
* therefore false: there is no shell escape.
|
|
4
|
+
* Declares the operator-chat surface for capabilities reporting and red-team
|
|
5
|
+
* checks: full Pi builtins (read/write/edit/bash/grep/find/ls), model-callable
|
|
6
|
+
* operator actions, and optional safe-* explore tools. hasBash is true.
|
|
7
|
+
* Non-Pi write channels (apply_patch / full-tools / shell / coding-chat) remain
|
|
8
|
+
* denied. Interview/Official loaders are unchanged.
|
|
12
9
|
*
|
|
13
10
|
* Note: this adapter is NOT the SDK's own ResourceLoader (that is
|
|
14
11
|
* DefaultResourceLoader, created inside createAgentSessionServices with
|
|
@@ -17,13 +14,12 @@
|
|
|
17
14
|
* The three-gate authorization (authorizeOperatorChatTool) is the actual
|
|
18
15
|
* enforcement; this adapter describes what that gate admits.
|
|
19
16
|
*/
|
|
20
|
-
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, authorizeOperatorChatTool, filterOperatorChatTools, assertNoWriteToolInList, } from "./tools.js";
|
|
21
|
-
/** Built-in
|
|
17
|
+
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, OPERATOR_CHAT_PI_BUILTIN_TOOLS, authorizeOperatorChatTool, filterOperatorChatTools, assertNoWriteToolInList, } from "./tools.js";
|
|
18
|
+
/** Built-in Pi tool names allowed in every Chat session (ADR 0011). */
|
|
22
19
|
export const OPERATOR_CHAT_BUILTIN_EXPLORE_TOOL_IDS = Object.freeze([
|
|
23
|
-
|
|
24
|
-
"ls",
|
|
20
|
+
...OPERATOR_CHAT_PI_BUILTIN_TOOLS,
|
|
25
21
|
]);
|
|
26
|
-
/** Safe explore custom tool names (
|
|
22
|
+
/** Safe explore custom tool names (optional parallel surface). */
|
|
27
23
|
export const OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS = Object.freeze([
|
|
28
24
|
"safe-read",
|
|
29
25
|
"safe-grep",
|
|
@@ -41,11 +37,6 @@ export function createOperatorChatResourceLoader() {
|
|
|
41
37
|
safeExploreToolIds: OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
|
|
42
38
|
listTools: () => OPERATOR_CHAT_ALLOWED_TOOLS,
|
|
43
39
|
tryActivateTool: (toolId) => {
|
|
44
|
-
// Explore tools (bash/read/grep/find/ls) are run by the SDK's built-in
|
|
45
|
-
// tool registry, not by the operator dispatcher. tryActivateTool only
|
|
46
|
-
// classifies operator actions here, but it must NOT deny an explore
|
|
47
|
-
// tool — that would contradict the real session. Deny only write tools
|
|
48
|
-
// and report unknowns.
|
|
49
40
|
const explore = OPERATOR_CHAT_BUILTIN_EXPLORE_TOOL_IDS.includes(toolId);
|
|
50
41
|
if (explore) {
|
|
51
42
|
return { ok: true, toolId };
|
|
@@ -62,6 +53,6 @@ export function createOperatorChatResourceLoader() {
|
|
|
62
53
|
return { active: [...active], denied };
|
|
63
54
|
},
|
|
64
55
|
allowsEnvToolRestore: false,
|
|
65
|
-
hasBash:
|
|
56
|
+
hasBash: true,
|
|
66
57
|
};
|
|
67
58
|
}
|
|
@@ -176,9 +176,8 @@ export async function handleChatCapabilities(_req, res, deps) {
|
|
|
176
176
|
skipped: skills.skipped,
|
|
177
177
|
promptFragmentCharCount: composeInstructionSkillsPrompt(skills.loaded).length,
|
|
178
178
|
},
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
hasBash: false,
|
|
179
|
+
// ADR 0011: full Pi tools including bash are default-active.
|
|
180
|
+
hasBash: true,
|
|
182
181
|
});
|
|
183
182
|
}
|
|
184
183
|
export async function handleCreateChatSession(req, res, deps) {
|
|
@@ -236,7 +235,7 @@ export async function handleCreateChatSession(req, res, deps) {
|
|
|
236
235
|
sessionFile: handle.sessionFile,
|
|
237
236
|
model: handle.model,
|
|
238
237
|
activeTools: handle.activeTools,
|
|
239
|
-
hasBash:
|
|
238
|
+
hasBash: true,
|
|
240
239
|
});
|
|
241
240
|
}
|
|
242
241
|
catch (error) {
|
|
@@ -342,7 +341,7 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
|
342
341
|
composerDraft,
|
|
343
342
|
activeTurnId: deps.events.getActiveTurn(sessionId)?.turnId,
|
|
344
343
|
},
|
|
345
|
-
hasBash:
|
|
344
|
+
hasBash: true,
|
|
346
345
|
lastEventId: lastEvent?.eventId ?? null,
|
|
347
346
|
lastEventSeq: lastEvent?.seq ?? 0,
|
|
348
347
|
});
|
|
@@ -356,7 +355,7 @@ export async function handleListChatSessions(_req, res, deps) {
|
|
|
356
355
|
...session,
|
|
357
356
|
activeTurnId: deps.events.getActiveTurn(session.sessionId)?.turnId,
|
|
358
357
|
})),
|
|
359
|
-
hasBash:
|
|
358
|
+
hasBash: true,
|
|
360
359
|
});
|
|
361
360
|
}
|
|
362
361
|
export async function handlePatchChatSession(req, res, deps, sessionId) {
|
|
@@ -395,7 +394,7 @@ export async function handleReopenChatSession(req, res, deps, sessionId) {
|
|
|
395
394
|
return;
|
|
396
395
|
}
|
|
397
396
|
if (deps.runtime.hasSession(sessionId)) {
|
|
398
|
-
sendJson(res, 200, { ok: true, sessionId, reopened: false, alreadyActive: true, hasBash:
|
|
397
|
+
sendJson(res, 200, { ok: true, sessionId, reopened: false, alreadyActive: true, hasBash: true });
|
|
399
398
|
return;
|
|
400
399
|
}
|
|
401
400
|
const result = await deps.runtime.reopenSession({
|
|
@@ -416,7 +415,7 @@ export async function handleReopenChatSession(req, res, deps, sessionId) {
|
|
|
416
415
|
sessionFile: result.handle.sessionFile,
|
|
417
416
|
model: result.handle.model,
|
|
418
417
|
activeTools: result.handle.activeTools,
|
|
419
|
-
hasBash:
|
|
418
|
+
hasBash: true,
|
|
420
419
|
});
|
|
421
420
|
}
|
|
422
421
|
export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
@@ -16,7 +16,7 @@ export function projectRuntimeContext(input) {
|
|
|
16
16
|
summary: summary(input.systemPrompt),
|
|
17
17
|
},
|
|
18
18
|
skills: input.skills.map((skill) => ({ ...skill, description: summary(skill.description, 200) ?? "" })),
|
|
19
|
-
resources: { mode: "closed", noContextFiles: true, noSkills: true, noExtensions: true, hasBash:
|
|
19
|
+
resources: { mode: "closed", noContextFiles: true, noSkills: true, noExtensions: true, hasBash: true, activeToolCount: input.activeTools.length },
|
|
20
20
|
model: input.model,
|
|
21
21
|
thinkingLevel: input.thinkingLevel,
|
|
22
22
|
suffix: input.systemPromptSuffix ? { present: true, summary: summary(input.systemPromptSuffix) } : { present: false },
|