sortie-dogs 0.14.2 → 0.14.3
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/README.md +6 -6
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/goal-bound.d.ts +8 -0
- package/dist/core/goal-bound.js +7 -2
- package/dist/plugin/docker-validation.d.ts +14 -0
- package/dist/plugin/docker-validation.js +153 -0
- package/dist/plugin/frontierharness-agent-tool-registry.json +61 -0
- package/dist/plugin/index.js +61 -16
- package/dist/plugin/mission-review.d.ts +8 -0
- package/dist/plugin/mission-review.js +125 -25
- package/dist/plugin/operator-status-output.d.ts +2 -3
- package/dist/plugin/operator-status-output.js +42 -7
- package/dist/plugin/profiled.js +12 -6
- package/dist/plugin/protected-snapshot.d.ts +10 -0
- package/dist/plugin/protected-snapshot.js +28 -0
- package/dist/plugin/v2.d.ts +73 -0
- package/dist/plugin/v2.js +41 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ evidence-backed result, not more agents for their own sake.
|
|
|
19
19
|
[](https://www.npmjs.com/package/sortie-dogs)
|
|
20
20
|
[](LICENSE)
|
|
21
21
|
|
|
22
|
-
**Current release: [v0.14.
|
|
23
|
-
· [Release notes](docs/release-v0.14.
|
|
22
|
+
**Current release: [v0.14.3](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.14.3)**
|
|
23
|
+
· [Release notes](docs/release-v0.14.3.md) · [Execution vision](docs/v0.14.0-execution-vision.md)
|
|
24
24
|
|
|
25
25
|
[Quick start](#quick-start) · [Workflow](#mission-workflow) · [Models](#default-models) ·
|
|
26
26
|
[Measured results](#measured-results) · [Configuration](#configuration) · [Documentation](#documentation)
|
|
@@ -57,7 +57,7 @@ Use an existing Codex CLI signed in with **ChatGPT**. On Windows, PowerShell **7
|
|
|
57
57
|
available; POSIX uses bash. Sortie does not install Codex, start login or use a metered API fallback.
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
|
-
npm install --save-dev sortie-dogs@0.14.
|
|
60
|
+
npm install --save-dev sortie-dogs@0.14.3
|
|
61
61
|
npx --no-install sortie-dogs codex init .
|
|
62
62
|
```
|
|
63
63
|
|
|
@@ -86,7 +86,7 @@ bridge. [Codex guide: SDK, permissions, progress, resume and manifest route](doc
|
|
|
86
86
|
Use **OpenCode V2**. In the target project:
|
|
87
87
|
|
|
88
88
|
```sh
|
|
89
|
-
npm install --save-dev sortie-dogs@0.14.
|
|
89
|
+
npm install --save-dev sortie-dogs@0.14.3
|
|
90
90
|
npx --no-install sortie-dogs init .
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -242,7 +242,7 @@ Install the desired package version and rerun **your host's** initializer:
|
|
|
242
242
|
|
|
243
243
|
An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
|
|
244
244
|
update it. [Global/update instructions](docs/configuration.md#global-availability).
|
|
245
|
-
The current Mission asset marker is `0.14.
|
|
245
|
+
The current Mission asset marker is `0.14.3-workspace-proof-v1`; the unchanged Codex skill marker is
|
|
246
246
|
`0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
|
|
247
247
|
|
|
248
248
|
There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
|
|
@@ -258,7 +258,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
|
|
|
258
258
|
[SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
|
|
259
259
|
- **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
|
|
260
260
|
[Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
|
|
261
|
-
- **Releases:** [v0.14.2](docs/release-v0.14.2.md) · [v0.14.1](docs/release-v0.14.1.md) · [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
261
|
+
- **Releases:** [v0.14.3](docs/release-v0.14.3.md) · [v0.14.2](docs/release-v0.14.2.md) · [v0.14.1](docs/release-v0.14.1.md) · [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
262
262
|
[v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
|
|
263
263
|
|
|
264
264
|
## Community
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export declare const V010_RUNTIME_ASSET_VERSION = "0.14.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.14.3-workspace-proof-v1";
|
|
7
7
|
export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
8
8
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export const V010_RUNTIME_ASSET_VERSION = "0.14.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.14.3-workspace-proof-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
@@ -45,6 +45,14 @@ export interface GoalEvidence {
|
|
|
45
45
|
/** Non-generating native validation pins inputs/concrete outputs, not write-only directory bookkeeping.
|
|
46
46
|
* Missing on old evidence: retain its full source/output recipe. */
|
|
47
47
|
readonly validation_policy?: "inputs-and-concrete-outputs-v1";
|
|
48
|
+
/** Snapshot and recheck the exact Git workspace inside one validated Docker task container. */
|
|
49
|
+
readonly docker_workspace?: {
|
|
50
|
+
readonly container_id: string;
|
|
51
|
+
readonly cwd: string;
|
|
52
|
+
readonly repository_root: string;
|
|
53
|
+
readonly source: string;
|
|
54
|
+
readonly candidate: string;
|
|
55
|
+
};
|
|
48
56
|
/** Fixed when validation starts. Full manifest_hash still identifies the historical execution contract. */
|
|
49
57
|
readonly freshness?: {
|
|
50
58
|
readonly contract_hash: string;
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -59,6 +59,7 @@ export function validGoalEvidence(value, state) {
|
|
|
59
59
|
return false;
|
|
60
60
|
const criteria = state.acceptance_contract?.criteria.filter(({ criterion_id }) => value.measurement.criterion_ids.includes(criterion_id)) ?? [];
|
|
61
61
|
const protectedBinding = value.protected_binding;
|
|
62
|
+
const dockerWorkspace = protectedBinding?.docker_workspace;
|
|
62
63
|
const protectedBindingValid = protectedBinding !== undefined && HASH.test(protectedBinding.manifest_hash) &&
|
|
63
64
|
text(protectedBinding.project_root) && text(protectedBinding.manifest_path) && Array.isArray(protectedBinding.source_paths) &&
|
|
64
65
|
protectedBinding.source_paths.every(text) && Array.isArray(protectedBinding.candidate_paths) &&
|
|
@@ -67,6 +68,10 @@ export function validGoalEvidence(value, state) {
|
|
|
67
68
|
protectedBinding.source_policy === "declared-paths-v1") &&
|
|
68
69
|
(protectedBinding.candidate_policy === undefined || protectedBinding.candidate_policy === "project-root-artifacts-v1") &&
|
|
69
70
|
(protectedBinding.validation_policy === undefined || protectedBinding.validation_policy === "inputs-and-concrete-outputs-v1") &&
|
|
71
|
+
(dockerWorkspace === undefined || (typeof dockerWorkspace.container_id === "string" && /^[a-f0-9]{12,64}$/u.test(dockerWorkspace.container_id) &&
|
|
72
|
+
typeof dockerWorkspace.cwd === "string" && dockerWorkspace.cwd.startsWith("/") &&
|
|
73
|
+
typeof dockerWorkspace.repository_root === "string" && dockerWorkspace.repository_root.startsWith("/") &&
|
|
74
|
+
HASH.test(dockerWorkspace.source) && HASH.test(dockerWorkspace.candidate))) &&
|
|
70
75
|
(protectedBinding.freshness === undefined || (protectedBinding.freshness !== null && typeof protectedBinding.freshness === "object" &&
|
|
71
76
|
HASH.test(protectedBinding.freshness.contract_hash) &&
|
|
72
77
|
Array.isArray(protectedBinding.freshness.scratch_paths) && protectedBinding.freshness.scratch_paths.every(text) &&
|
|
@@ -89,10 +94,10 @@ export function validGoalEvidence(value, state) {
|
|
|
89
94
|
sameStrings(criterion.oracle_coverage, value.measurement.oracle_coverage) &&
|
|
90
95
|
criterion.build_boundary === value.measurement.build_boundary &&
|
|
91
96
|
(criterion.source_binding === "current-protected"
|
|
92
|
-
? protectedBindingValid && HASH.test(value.identity.source)
|
|
97
|
+
? protectedBindingValid && HASH.test(value.identity.source) && (dockerWorkspace === undefined || dockerWorkspace.source === value.identity.source)
|
|
93
98
|
: criterion.source === value.identity.source) &&
|
|
94
99
|
(criterion.candidate_binding === "current-protected"
|
|
95
|
-
? protectedBindingValid && HASH.test(value.identity.candidate)
|
|
100
|
+
? protectedBindingValid && HASH.test(value.identity.candidate) && (dockerWorkspace === undefined || dockerWorkspace.candidate === value.identity.candidate)
|
|
96
101
|
: criterion.candidate === value.identity.candidate) &&
|
|
97
102
|
(criterion.validation_command === undefined ||
|
|
98
103
|
(value.execution.command.length === 1 && value.execution.command[0] === criterion.validation_command)) &&
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DockerWorkspaceSnapshot {
|
|
2
|
+
readonly container_id: string;
|
|
3
|
+
readonly cwd: string;
|
|
4
|
+
readonly repository_root: string;
|
|
5
|
+
readonly source: string;
|
|
6
|
+
readonly candidate: string;
|
|
7
|
+
}
|
|
8
|
+
/** Hash the fixed container's actual workspace, including non-Git and ignored inputs. */
|
|
9
|
+
export declare function captureDockerWorkspaceSnapshot(containerID: string, cwd: string): Promise<DockerWorkspaceSnapshot | undefined>;
|
|
10
|
+
export declare function isDockerContainerID(value: unknown): value is string;
|
|
11
|
+
export declare function isDockerWorkspacePath(value: unknown): value is string;
|
|
12
|
+
/** Inspect bytes, never execute caller-supplied code. The Review envelope's
|
|
13
|
+
* immutable Docker candidate must match both before and after the read. */
|
|
14
|
+
export declare function readDockerWorkspaceFile(containerID: string, cwd: string, candidate: string, path: string, offset?: number, limit?: number): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { posix } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const exec = promisify(execFile);
|
|
6
|
+
const CONTAINER_ID = /^[a-f0-9]{12,64}$/u;
|
|
7
|
+
const SNAPSHOT_MARKER = "SORTIE_DOCKER_WORKSPACE_V1";
|
|
8
|
+
// Terminal tasks need not contain Git at all. Capture actual bytes and file
|
|
9
|
+
// identity, including ignored inputs; timestamps alone are not candidate edits.
|
|
10
|
+
const FILE_SNAPSHOT = String.raw `
|
|
11
|
+
import hashlib, json, os, stat, subprocess
|
|
12
|
+
cwd = os.path.realpath(os.getcwd())
|
|
13
|
+
try:
|
|
14
|
+
root = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], stderr=subprocess.DEVNULL, text=True).strip()
|
|
15
|
+
head = subprocess.check_output(["git", "rev-parse", "--verify", "HEAD^{tree}"], stderr=subprocess.DEVNULL, text=True).strip()
|
|
16
|
+
except (OSError, subprocess.CalledProcessError):
|
|
17
|
+
root, head = cwd, "no-git"
|
|
18
|
+
root = os.path.realpath(root)
|
|
19
|
+
rows = []
|
|
20
|
+
def digest(path):
|
|
21
|
+
h = hashlib.sha256()
|
|
22
|
+
with open(path, "rb") as stream:
|
|
23
|
+
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
24
|
+
h.update(block)
|
|
25
|
+
return h.hexdigest()
|
|
26
|
+
for directory, dirs, files in os.walk(root, followlinks=False, onerror=lambda error: (_ for _ in ()).throw(error)):
|
|
27
|
+
if directory == root:
|
|
28
|
+
dirs[:] = [name for name in dirs if name != ".git"]
|
|
29
|
+
files = [name for name in files if name != ".git"]
|
|
30
|
+
for name in [".", *dirs, *files]:
|
|
31
|
+
path = directory if name == "." else os.path.join(directory, name)
|
|
32
|
+
relative = os.path.relpath(path, root)
|
|
33
|
+
info = os.lstat(path)
|
|
34
|
+
mode = stat.S_IMODE(info.st_mode)
|
|
35
|
+
if stat.S_ISREG(info.st_mode):
|
|
36
|
+
value = [relative, "file", mode, digest(path)]
|
|
37
|
+
elif stat.S_ISLNK(info.st_mode):
|
|
38
|
+
target = os.path.realpath(path)
|
|
39
|
+
# Internal directory targets are captured at their own lexical path.
|
|
40
|
+
# External directory trees are not silently omitted from a proof.
|
|
41
|
+
if os.path.isdir(path) and os.path.commonpath([root, target]) != root:
|
|
42
|
+
raise RuntimeError("external-directory-link-snapshot-unavailable")
|
|
43
|
+
value = [relative, "link", mode, os.readlink(path), digest(path) if os.path.isfile(path) else None]
|
|
44
|
+
elif stat.S_ISDIR(info.st_mode):
|
|
45
|
+
value = [relative, "directory", mode]
|
|
46
|
+
else:
|
|
47
|
+
raise RuntimeError("unsupported-workspace-file-type")
|
|
48
|
+
rows.append(value)
|
|
49
|
+
tree = hashlib.sha256(json.dumps(sorted(rows), ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
|
|
50
|
+
print("SORTIE_DOCKER_WORKSPACE_V1")
|
|
51
|
+
print(cwd)
|
|
52
|
+
print(root)
|
|
53
|
+
print(tree)
|
|
54
|
+
print(hashlib.sha256(head.encode()).hexdigest())
|
|
55
|
+
print("0" * 64)
|
|
56
|
+
`;
|
|
57
|
+
const SNAPSHOT_COMMAND = [
|
|
58
|
+
"set -eu",
|
|
59
|
+
"if command -v python3 >/dev/null 2>&1; then",
|
|
60
|
+
"python3 -I -S - <<'SORTIE_WORKSPACE_PY'",
|
|
61
|
+
FILE_SNAPSHOT,
|
|
62
|
+
"SORTIE_WORKSPACE_PY",
|
|
63
|
+
"else",
|
|
64
|
+
"actual_cwd=$(pwd -P)",
|
|
65
|
+
"root=$(git rev-parse --show-toplevel)",
|
|
66
|
+
"cd \"$root\"",
|
|
67
|
+
"tree=$(git rev-parse --verify 'HEAD^{tree}')",
|
|
68
|
+
"tracked=$(git diff --binary HEAD -- | sha256sum | cut -d' ' -f1)",
|
|
69
|
+
"untracked=$(git ls-files -z --others | sort -z | xargs -0 -r sha256sum -- | sha256sum | cut -d' ' -f1)",
|
|
70
|
+
"printf '%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n' 'SORTIE_DOCKER_WORKSPACE_V1' \"$actual_cwd\" \"$root\" \"$tree\" \"$tracked\" \"$untracked\"",
|
|
71
|
+
"fi",
|
|
72
|
+
].join("\n");
|
|
73
|
+
/** Hash the fixed container's actual workspace, including non-Git and ignored inputs. */
|
|
74
|
+
export async function captureDockerWorkspaceSnapshot(containerID, cwd) {
|
|
75
|
+
if (!CONTAINER_ID.test(containerID) || !posix.isAbsolute(cwd) || posix.normalize(cwd) !== cwd || cwd.includes("\0"))
|
|
76
|
+
return undefined;
|
|
77
|
+
try {
|
|
78
|
+
const result = await exec("docker", ["exec", "--workdir", cwd, containerID, "sh", "-lc", SNAPSHOT_COMMAND], {
|
|
79
|
+
encoding: "buffer", timeout: 30_000, maxBuffer: 64 * 1024 * 1024,
|
|
80
|
+
});
|
|
81
|
+
const output = result.stdout;
|
|
82
|
+
let offset = 0;
|
|
83
|
+
const line = () => {
|
|
84
|
+
const end = output.indexOf(0x0a, offset);
|
|
85
|
+
if (end < 0)
|
|
86
|
+
return undefined;
|
|
87
|
+
const value = output.subarray(offset, end).toString("utf8");
|
|
88
|
+
offset = end + 1;
|
|
89
|
+
return value;
|
|
90
|
+
};
|
|
91
|
+
if (line() !== SNAPSHOT_MARKER)
|
|
92
|
+
return undefined;
|
|
93
|
+
const actualCwd = line(), repositoryRoot = line(), tree = line(), tracked = line(), untracked = line();
|
|
94
|
+
if (actualCwd !== cwd || !repositoryRoot || !posix.isAbsolute(repositoryRoot) || !tree || !/^[a-f0-9]{40,64}$/u.test(tree) ||
|
|
95
|
+
!tracked || !/^[a-f0-9]{64}$/u.test(tracked) || !untracked || !/^[a-f0-9]{64}$/u.test(untracked) || offset !== output.length)
|
|
96
|
+
return undefined;
|
|
97
|
+
const source = `sha256:${createHash("sha256").update(`git-tree\0${repositoryRoot}\0${tree}`).digest("hex")}`;
|
|
98
|
+
const candidate = `sha256:${createHash("sha256").update(JSON.stringify([containerID, cwd, repositoryRoot, tree, tracked, untracked])).digest("hex")}`;
|
|
99
|
+
return { container_id: containerID, cwd, repository_root: repositoryRoot, source, candidate };
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function isDockerContainerID(value) {
|
|
106
|
+
return typeof value === "string" && CONTAINER_ID.test(value);
|
|
107
|
+
}
|
|
108
|
+
export function isDockerWorkspacePath(value) {
|
|
109
|
+
return typeof value === "string" && value.length > 0 && posix.isAbsolute(value) &&
|
|
110
|
+
posix.normalize(value) === value && !value.includes("\0");
|
|
111
|
+
}
|
|
112
|
+
const READ_WORKSPACE = String.raw `
|
|
113
|
+
import hashlib, json, os, stat, sys
|
|
114
|
+
root, requested, offset, limit = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4])
|
|
115
|
+
path = os.path.realpath(os.path.join(root, requested))
|
|
116
|
+
if os.path.commonpath([root, path]) != root:
|
|
117
|
+
raise ValueError("file-outside-candidate-workspace")
|
|
118
|
+
if os.path.isdir(path):
|
|
119
|
+
entries = sorted(os.listdir(path))
|
|
120
|
+
result = {"kind": "directory", "entries": entries[offset - 1:offset - 1 + limit], "total": len(entries)}
|
|
121
|
+
elif stat.S_ISREG(os.stat(path).st_mode):
|
|
122
|
+
h = hashlib.sha256()
|
|
123
|
+
lines, total, size = [], 0, 0
|
|
124
|
+
with open(path, "rb") as stream:
|
|
125
|
+
for line in stream:
|
|
126
|
+
h.update(line)
|
|
127
|
+
total += 1
|
|
128
|
+
if offset <= total < offset + limit and size < 64000:
|
|
129
|
+
text = line[:64000-size].decode("utf-8", errors="replace")
|
|
130
|
+
lines.append([total, text])
|
|
131
|
+
size += len(line[:64000-size])
|
|
132
|
+
result = {"kind": "file", "lines": lines, "total_lines": total, "sha256": h.hexdigest()}
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError("candidate-file-not-readable")
|
|
135
|
+
print(json.dumps({"path": path, **result}, ensure_ascii=True))
|
|
136
|
+
`;
|
|
137
|
+
/** Inspect bytes, never execute caller-supplied code. The Review envelope's
|
|
138
|
+
* immutable Docker candidate must match both before and after the read. */
|
|
139
|
+
export async function readDockerWorkspaceFile(containerID, cwd, candidate, path, offset = 1, limit = 200) {
|
|
140
|
+
if (!/^sha256:[a-f0-9]{64}$/u.test(candidate) || typeof path !== "string" || path.includes("\0") ||
|
|
141
|
+
!Number.isSafeInteger(offset) || offset < 1 || !Number.isSafeInteger(limit) || limit < 1 || limit > 2000) {
|
|
142
|
+
throw new Error("docker-candidate-read-invalid");
|
|
143
|
+
}
|
|
144
|
+
const before = await captureDockerWorkspaceSnapshot(containerID, cwd);
|
|
145
|
+
if (!before || before.candidate !== candidate)
|
|
146
|
+
throw new Error("docker-review-candidate-stale-or-unavailable");
|
|
147
|
+
const { stdout } = await exec("docker", ["exec", "--workdir", cwd, containerID, "python3", "-I", "-S", "-c", READ_WORKSPACE,
|
|
148
|
+
before.repository_root, path, String(offset), String(limit)], { encoding: "utf8", timeout: 30_000, maxBuffer: 256 * 1024 });
|
|
149
|
+
const after = await captureDockerWorkspaceSnapshot(containerID, cwd);
|
|
150
|
+
if (!after || after.candidate !== candidate)
|
|
151
|
+
throw new Error("docker-review-candidate-changed-during-read");
|
|
152
|
+
return { workspace: before, ...JSON.parse(stdout) };
|
|
153
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"tool_prefix": "sortie_v010_",
|
|
4
|
+
"core_tools": ["execute", "subagent", "read"],
|
|
5
|
+
"runtime_agents": {
|
|
6
|
+
"dog-coordinator": "dog-operator",
|
|
7
|
+
"dog-worker": "dog-worker-v010",
|
|
8
|
+
"dog-luna-worker": "dog-luna-worker-v010",
|
|
9
|
+
"dog-scout": "dog-scout-v010",
|
|
10
|
+
"dog-reviewer": "dog-reviewer-v010",
|
|
11
|
+
"dog-advisor": "dog-advisor-v010",
|
|
12
|
+
"dog-operator": "dogs-coordinator"
|
|
13
|
+
},
|
|
14
|
+
"roles": {
|
|
15
|
+
"dog-coordinator": {
|
|
16
|
+
"tool_families": {
|
|
17
|
+
"mission_lifecycle": ["start_mission", "operator_next", "operator_status", "extend_mission_budget", "complete_mission", "cancel_operator", "reflection"],
|
|
18
|
+
"unit_execution": ["plan_units", "start_direct_unit", "finish_direct_unit", "retry_mission_unit", "expand_unit"],
|
|
19
|
+
"review": ["review_mission", "repair_review"]
|
|
20
|
+
},
|
|
21
|
+
"children": ["dog-operator", "dog-worker", "dog-reviewer"]
|
|
22
|
+
},
|
|
23
|
+
"dog-worker": {
|
|
24
|
+
"tool_families": {
|
|
25
|
+
"workspace": ["bind_write_gate", "release_write_gate", "operator_status", "expand_unit"]
|
|
26
|
+
},
|
|
27
|
+
"native_tool_families": {
|
|
28
|
+
"docker_task": ["fh_docker_exec"]
|
|
29
|
+
},
|
|
30
|
+
"core_tools": ["execute", "read"],
|
|
31
|
+
"children": []
|
|
32
|
+
},
|
|
33
|
+
"dog-luna-worker": {
|
|
34
|
+
"tool_families": {
|
|
35
|
+
"workspace": ["bind_write_gate", "release_write_gate", "operator_status", "expand_unit"]
|
|
36
|
+
},
|
|
37
|
+
"core_tools": ["execute", "read"],
|
|
38
|
+
"children": []
|
|
39
|
+
},
|
|
40
|
+
"dog-scout": { "tool_families": {}, "children": [] },
|
|
41
|
+
"dog-reviewer": {
|
|
42
|
+
"tool_families": { "review": ["repair_review"] },
|
|
43
|
+
"native_tool_families": { "docker_review": ["fh_docker_read"] },
|
|
44
|
+
"correction_native_tool_families": { "docker_task": ["fh_docker_exec"] },
|
|
45
|
+
"correction_tool_families": {
|
|
46
|
+
"workspace": ["bind_write_gate", "release_write_gate", "operator_status", "expand_unit"],
|
|
47
|
+
"correction": ["repair_review", "finish_direct_unit"]
|
|
48
|
+
},
|
|
49
|
+
"children": []
|
|
50
|
+
},
|
|
51
|
+
"dog-advisor": { "tool_families": {}, "children": [] },
|
|
52
|
+
"dog-operator": {
|
|
53
|
+
"tool_families": {
|
|
54
|
+
"mission_lifecycle": ["operator_next", "operator_status", "submit_mission", "skip_mission_consultation"],
|
|
55
|
+
"unit_execution": ["plan_units", "start_direct_unit", "finish_direct_unit", "retry_mission_unit", "rescue_mission_unit", "expand_unit"],
|
|
56
|
+
"review": ["review_mission", "repair_review"]
|
|
57
|
+
},
|
|
58
|
+
"children": ["dog-worker", "dog-reviewer"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/plugin/index.js
CHANGED
|
@@ -53,7 +53,8 @@ import { STABLE_RUNTIME_PROFILE } from "../core/runtime-profile.js";
|
|
|
53
53
|
import { reviewerCorrectionValidationFresh } from "./mission-review.js";
|
|
54
54
|
import { evidenceFromObservedExecution } from "../core/observed-goal-evidence.js";
|
|
55
55
|
import { receiptBoundTerminalText } from "./receipt-presentation.js";
|
|
56
|
-
import { operationInputSnapshot, protectedSnapshot, refreshProtectedSnapshot, validatedSourceSnapshot, validationInputSnapshot } from "./protected-snapshot.js";
|
|
56
|
+
import { operationInputSnapshot, protectedDockerSnapshot, protectedSnapshot, refreshProtectedSnapshot, validatedSourceSnapshot, validationInputSnapshot } from "./protected-snapshot.js";
|
|
57
|
+
import { isDockerContainerID, isDockerWorkspacePath } from "./docker-validation.js";
|
|
57
58
|
import { settledUnitUsage } from "./unit-usage.js";
|
|
58
59
|
import { goalCompletionReadiness } from "./goal-completion.js";
|
|
59
60
|
const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
|
|
@@ -205,10 +206,34 @@ function observedHostGoalEvidence(execution, goal, unitID) {
|
|
|
205
206
|
function isRecord(value) {
|
|
206
207
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
207
208
|
}
|
|
208
|
-
function validationEnvironment(directory) {
|
|
209
|
+
function validationEnvironment(directory, dockerWorkspace) {
|
|
210
|
+
if (dockerWorkspace)
|
|
211
|
+
return { platform: "docker-container", arch: "unknown", runtime: "docker",
|
|
212
|
+
toolchain: `container:${dockerWorkspace.container_id}`, directory: dockerWorkspace.cwd };
|
|
209
213
|
return { platform: process.platform, arch: process.arch, runtime: process.version,
|
|
210
214
|
...(directory ? { directory } : {}) };
|
|
211
215
|
}
|
|
216
|
+
function dockerToolExitCode(output) {
|
|
217
|
+
const metadata = isRecord(output.metadata) ? output.metadata : undefined;
|
|
218
|
+
const status = output.status ?? metadata?.status;
|
|
219
|
+
if (status === "error" || status === "cancel" || status === "cancelled" || status === "canceled" || status === "interrupted")
|
|
220
|
+
return undefined;
|
|
221
|
+
const recorded = metadata?.exit;
|
|
222
|
+
const text = typeof output.output === "string" ? output.output
|
|
223
|
+
: typeof output.content === "string" ? output.content
|
|
224
|
+
: Array.isArray(output.content) ? output.content.flatMap(part => isRecord(part) && typeof part.text === "string" ? [part.text] : []).join("\n")
|
|
225
|
+
: "";
|
|
226
|
+
const match = text.match(/^exit_code=(0|[1-9]\d*|-[1-9]\d*|unknown)(?:\r?\n|$)/u);
|
|
227
|
+
const header = match?.[1] === "unknown" ? null : match ? Number(match[1]) : undefined;
|
|
228
|
+
const explicit = typeof recorded === "number" && Number.isSafeInteger(recorded) ? recorded : undefined;
|
|
229
|
+
if (explicit !== undefined && header !== undefined && explicit !== header)
|
|
230
|
+
return undefined;
|
|
231
|
+
if (explicit !== undefined)
|
|
232
|
+
return explicit;
|
|
233
|
+
if (header !== undefined && (header === null || Number.isSafeInteger(header)))
|
|
234
|
+
return header;
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
212
237
|
function validationCandidate(source, candidate) {
|
|
213
238
|
return goalFingerprint({ source, candidate });
|
|
214
239
|
}
|
|
@@ -1347,11 +1372,17 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1347
1372
|
if (authorization === undefined || authorization.suspended || authorization.rootSessionID !== owner)
|
|
1348
1373
|
return;
|
|
1349
1374
|
const args = isRecord(output.args) ? output.args : undefined;
|
|
1375
|
+
const dockerTool = toolInput.tool === "fh_docker_exec" || toolInput.tool === "fh.docker_exec";
|
|
1376
|
+
const dockerRequest = dockerTool && args !== undefined && isDockerContainerID(args.container_id) &&
|
|
1377
|
+
isDockerWorkspacePath(args.cwd) && typeof args.command === "string"
|
|
1378
|
+
? { containerID: args.container_id, cwd: args.cwd } : undefined;
|
|
1379
|
+
if (dockerTool && dockerRequest === undefined)
|
|
1380
|
+
return;
|
|
1350
1381
|
const rawCommand = args !== undefined && typeof args.command === "string" ? normalizeCommand(args.command) : undefined;
|
|
1351
1382
|
if (rawCommand === undefined || rawCommand.length === 0)
|
|
1352
1383
|
return;
|
|
1353
1384
|
const correcting = !!direct || await input.runtimeBridge?.ownsReviewerCorrection?.(toolInput.sessionID) === true;
|
|
1354
|
-
const directory = resolve(input.directory, typeof args?.workdir === "string" ? args.workdir : ".");
|
|
1385
|
+
const directory = dockerRequest?.cwd ?? resolve(input.directory, typeof args?.workdir === "string" ? args.workdir : ".");
|
|
1355
1386
|
// A check is proof only in its registered cwd. Snapshot the same admitted source
|
|
1356
1387
|
// recipe at actual native start; never retrofit it to older external-cwd history.
|
|
1357
1388
|
const members = correcting ? await input.runtimeBridge?.reviewerCorrectionValidationMembers?.(toolInput.sessionID, rawCommand, directory)
|
|
@@ -1367,9 +1398,14 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1367
1398
|
? await input.runtimeBridge?.missionDispatchCall?.(authorization.rootSessionID, toolInput.sessionID, authorization.taskID) : undefined);
|
|
1368
1399
|
const correction = correcting && dispatchCallID && members
|
|
1369
1400
|
? { taskID: authorization.taskID, dispatchCallID, commands: members } : undefined;
|
|
1370
|
-
const admittedSnapshot =
|
|
1401
|
+
const admittedSnapshot = dockerRequest
|
|
1402
|
+
? await protectedDockerSnapshot(authorization, dockerRequest.containerID, dockerRequest.cwd).catch(() => undefined)
|
|
1403
|
+
: await protectedSnapshot(authorization, { directory, command: args.command }).catch(() => undefined);
|
|
1371
1404
|
if (admittedSnapshot === undefined)
|
|
1372
1405
|
return;
|
|
1406
|
+
const dockerWorkspace = admittedSnapshot.binding.docker_workspace;
|
|
1407
|
+
if (dockerRequest && (!dockerWorkspace || dockerWorkspace.container_id !== dockerRequest.containerID || dockerWorkspace.cwd !== dockerRequest.cwd))
|
|
1408
|
+
return;
|
|
1373
1409
|
const root = goalRoot(owner);
|
|
1374
1410
|
const ledger = await goalLedger(root);
|
|
1375
1411
|
const goalSnapshot = await ledger.readGoal(), goal = goalSnapshot.state;
|
|
@@ -1410,7 +1446,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1410
1446
|
const owner = validationOwner(scope);
|
|
1411
1447
|
const request = { run_id: goal.goal_id, operation_id: toolInput.callID,
|
|
1412
1448
|
source_snapshot: snapshot.source, candidate: validationCandidate(snapshot.source, snapshot.candidate),
|
|
1413
|
-
command: [rawCommand], environment: validationEnvironment(directory), scope, owner,
|
|
1449
|
+
command: [rawCommand], environment: validationEnvironment(directory, dockerWorkspace), scope, owner,
|
|
1414
1450
|
expected_evidence: [...new Set(criteria.flatMap((criterion) => [criterion.criterion_id, ...criterion.oracle_coverage,
|
|
1415
1451
|
`unit:${unitID}`, "source_snapshot", "candidate", "command", "scope", "exit_code"]))],
|
|
1416
1452
|
marginal_value: { unmet_criteria: criteria.map(criterion => criterion.criterion_id), risk_hypothesis: null },
|
|
@@ -1418,12 +1454,17 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1418
1454
|
? { required_execution: { admission: dispatchCallID, call_id: toolInput.callID } } : {}) };
|
|
1419
1455
|
const evidenceKey = validationEvidenceKey(request);
|
|
1420
1456
|
const durable = goalSnapshot.records.flatMap(({ event }) => event.kind === "unit.settled" || event.kind === "unit.evidence-reconciled" ? event.evidence : [])
|
|
1421
|
-
.filter(entry => directory === resolve(input.directory)
|
|
1457
|
+
.filter(entry => (directory === resolve(input.directory) || dockerWorkspace !== undefined &&
|
|
1458
|
+
entry.protected_binding?.docker_workspace?.container_id === dockerWorkspace.container_id &&
|
|
1459
|
+
entry.protected_binding.docker_workspace.cwd === dockerWorkspace.cwd) && validGoalEvidence(entry, goal) &&
|
|
1460
|
+
(entry.proof_scope !== "requested-full" || coordinatorOwned) &&
|
|
1422
1461
|
entry.identity.source === snapshot.source && entry.identity.candidate === snapshot.candidate &&
|
|
1423
1462
|
entry.execution.command.length === 1 && entry.execution.command[0] === rawCommand)
|
|
1424
1463
|
.map(entry => ({ ...entry, execution: { ...entry.execution, units: [unitID] } }));
|
|
1425
1464
|
const live = [...hostGoalExecutions.values()].filter(execution => execution.root === root &&
|
|
1426
|
-
execution.sessionID === toolInput.sessionID && execution.directory === directory
|
|
1465
|
+
execution.sessionID === toolInput.sessionID && (execution.directory === directory || dockerWorkspace !== undefined &&
|
|
1466
|
+
execution.dockerWorkspace?.container_id === dockerWorkspace.container_id && execution.dockerWorkspace.cwd === dockerWorkspace.cwd) &&
|
|
1467
|
+
execution.endedAt !== undefined && execution.exitCode === 0 &&
|
|
1427
1468
|
execution.outcome === "pass" && execution.immutableRef !== undefined && execution.fresh === true &&
|
|
1428
1469
|
execution.source === snapshot.source && execution.candidate === snapshot.candidate &&
|
|
1429
1470
|
execution.command.length === 1 && execution.command[0] === rawCommand)
|
|
@@ -1463,7 +1504,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1463
1504
|
}
|
|
1464
1505
|
if (reservation.decision === "SKIP") {
|
|
1465
1506
|
reusedEvidence = reconciliationEvidence;
|
|
1466
|
-
const notice = settledPassNoticeCommand();
|
|
1507
|
+
const notice = dockerRequest ? "true" : settledPassNoticeCommand();
|
|
1467
1508
|
args.command = notice;
|
|
1468
1509
|
settledPassNotices.set(toolInput.callID, { sessionID: toolInput.sessionID, command: notice });
|
|
1469
1510
|
}
|
|
@@ -1475,10 +1516,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1475
1516
|
}
|
|
1476
1517
|
}
|
|
1477
1518
|
const mission = await new OperatorMissionRuntime(input.directory, runtimeProfile).read(root);
|
|
1478
|
-
const operationInputs = mission?.kind === "operation" && mission.execution?.commands.includes(rawCommand) &&
|
|
1519
|
+
const operationInputs = !dockerWorkspace && mission?.kind === "operation" && mission.execution?.commands.includes(rawCommand) &&
|
|
1479
1520
|
resolve(input.directory, typeof args?.workdir === "string" ? args.workdir : ".") === mission.execution.directory
|
|
1480
1521
|
? await operationInputSnapshot(authorization.projectRoot, snapshot.binding).catch(() => undefined) : undefined;
|
|
1481
|
-
const validationInputs = (validation !== undefined || correction !== undefined) && operationInputs === undefined
|
|
1522
|
+
const validationInputs = !dockerWorkspace && (validation !== undefined || correction !== undefined) && operationInputs === undefined
|
|
1482
1523
|
? await validationInputSnapshot(authorization.projectRoot, snapshot.binding).catch(() => undefined) : undefined;
|
|
1483
1524
|
const validationSource = correction && validationInputs !== undefined
|
|
1484
1525
|
? await validatedSourceSnapshot(authorization.projectRoot, snapshot.binding).catch(() => undefined) : undefined;
|
|
@@ -1486,6 +1527,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1486
1527
|
sessionID: toolInput.sessionID,
|
|
1487
1528
|
callID: toolInput.callID, tool: toolInput.tool, command: [rawCommand], directory, startedAt: new Date().toISOString(),
|
|
1488
1529
|
binding: snapshot.binding, source: snapshot.source, candidate: snapshot.candidate,
|
|
1530
|
+
...(dockerWorkspace === undefined ? {} : { dockerWorkspace }),
|
|
1489
1531
|
...(operationInputs === undefined ? {} : { operationInputs }),
|
|
1490
1532
|
...(validationInputs === undefined ? {} : { validationInputs }),
|
|
1491
1533
|
...(validationSource === undefined ? {} : { validationSource }),
|
|
@@ -1499,7 +1541,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1499
1541
|
return;
|
|
1500
1542
|
const metadata = isRecord(output.metadata) ? output.metadata : undefined;
|
|
1501
1543
|
const rawExit = metadata?.exit;
|
|
1502
|
-
const exitCode =
|
|
1544
|
+
const exitCode = execution.dockerWorkspace ? dockerToolExitCode(output)
|
|
1545
|
+
: typeof rawExit === "number" && Number.isSafeInteger(rawExit) ? rawExit : undefined;
|
|
1503
1546
|
const rawStatus = metadata?.status ?? output.status;
|
|
1504
1547
|
const outcome = exitCode === 0 ? execution.reusedEvidence === undefined ? "pass" : "skip" : exitCode !== undefined ? "fail"
|
|
1505
1548
|
: rawStatus === "cancel" || rawStatus === "cancelled" ? "cancel" : undefined;
|
|
@@ -1519,11 +1562,13 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1519
1562
|
? new Date(timing.end).toISOString() : new Date().toISOString();
|
|
1520
1563
|
// Declared outputs can change during a successful validation. Read inputs must not;
|
|
1521
1564
|
// the evidence still binds to the complete post-command candidate and source.
|
|
1522
|
-
const fresh = refreshed !== undefined && (execution.
|
|
1523
|
-
?
|
|
1524
|
-
: execution.
|
|
1525
|
-
? await
|
|
1526
|
-
:
|
|
1565
|
+
const fresh = refreshed !== undefined && (execution.dockerWorkspace !== undefined
|
|
1566
|
+
? refreshed.source === execution.source && refreshed.candidate === execution.candidate
|
|
1567
|
+
: execution.operationInputs !== undefined
|
|
1568
|
+
? await operationInputSnapshot(execution.projectRoot, execution.binding).catch(() => undefined) === execution.operationInputs
|
|
1569
|
+
: execution.validationInputs !== undefined
|
|
1570
|
+
? await validationInputSnapshot(execution.projectRoot, execution.binding).catch(() => undefined) === execution.validationInputs
|
|
1571
|
+
: refreshed.source === execution.source);
|
|
1527
1572
|
const immutableRef = outcome === undefined || execution.reusedEvidence !== undefined ? undefined : goalFingerprint({ root: execution.root,
|
|
1528
1573
|
child_session_id: execution.sessionID, call_id: execution.callID, command: execution.command, directory: execution.directory,
|
|
1529
1574
|
started_at: observedStartedAt, ended_at: endedAt, exit_code: exitCode ?? null, outcome,
|
|
@@ -33,6 +33,13 @@ export declare function missionReviewValidation(run: OperatorState, statePath: s
|
|
|
33
33
|
}[] | undefined;
|
|
34
34
|
evidence: {
|
|
35
35
|
protected_binding_ref?: {
|
|
36
|
+
docker_workspace?: {
|
|
37
|
+
readonly container_id: string;
|
|
38
|
+
readonly cwd: string;
|
|
39
|
+
readonly repository_root: string;
|
|
40
|
+
readonly source: string;
|
|
41
|
+
readonly candidate: string;
|
|
42
|
+
} | undefined;
|
|
36
43
|
manifest_path: string;
|
|
37
44
|
manifest_hash: string;
|
|
38
45
|
source_policy: "project-files-v1" | "declared-paths-v1" | undefined;
|
|
@@ -75,6 +82,7 @@ export declare function observedMissionValidation(validation: readonly string[],
|
|
|
75
82
|
command: string;
|
|
76
83
|
native_command?: string;
|
|
77
84
|
directory?: string;
|
|
85
|
+
directory_status?: "unobserved";
|
|
78
86
|
observed_members?: readonly string[];
|
|
79
87
|
exit_code: number | null;
|
|
80
88
|
started_ms: number | null;
|
|
@@ -12,6 +12,7 @@ import { taskChildSessionID } from "./task-result-repair.js";
|
|
|
12
12
|
import { normalizeManifestScope } from "../core/path.js";
|
|
13
13
|
import { declaredArtifacts } from "./declared-artifacts.js";
|
|
14
14
|
import { canonicalDeclaredValidationMembers, canonicalObservedValidationMembers, normalizeCommand } from "./gate.js";
|
|
15
|
+
import { isDockerContainerID, isDockerWorkspacePath } from "./docker-validation.js";
|
|
15
16
|
import { currentSnapshotProtection, isRuntimeControlPath, refreshProtectedSnapshot, snapshotControlExclusion, snapshotScratchExclusion, validationInputSnapshot } from "./protected-snapshot.js";
|
|
16
17
|
const exec = promisify(execFile);
|
|
17
18
|
const notRepository = (error) => /fatal: not a git repository \(or (?:any of the parent directories|any parent up to mount point [^\r\n)]+)\)(?:: \.git)?/u
|
|
@@ -48,6 +49,7 @@ export function missionReviewValidation(run, statePath) {
|
|
|
48
49
|
manifest_path: protected_binding.manifest_path,
|
|
49
50
|
manifest_hash: protected_binding.manifest_hash,
|
|
50
51
|
source_policy: protected_binding.source_policy,
|
|
52
|
+
...(protected_binding.docker_workspace ? { docker_workspace: protected_binding.docker_workspace } : {}),
|
|
51
53
|
} } : {}),
|
|
52
54
|
})),
|
|
53
55
|
...(unit.reviewerCorrection ? { correction_checks: (unit.reviewerCorrection.checks ?? []).map(check => ({
|
|
@@ -60,6 +62,30 @@ export function missionReviewValidation(run, statePath) {
|
|
|
60
62
|
field: "units[].evidence", omitted: "host snapshot recipe: source/candidate paths and freshness environment" },
|
|
61
63
|
}));
|
|
62
64
|
}
|
|
65
|
+
function nativeToolText(value) {
|
|
66
|
+
if (typeof value === "string")
|
|
67
|
+
return value;
|
|
68
|
+
if (!Array.isArray(value))
|
|
69
|
+
return undefined;
|
|
70
|
+
const texts = value.flatMap(item => record(item) && item.type === "text" && typeof item.text === "string" ? [item.text] : []);
|
|
71
|
+
return texts.length ? texts.join("\n") : undefined;
|
|
72
|
+
}
|
|
73
|
+
function dockerExecExitCode(state) {
|
|
74
|
+
if (state.status !== "completed")
|
|
75
|
+
return null;
|
|
76
|
+
const metadataStatus = record(state.metadata) ? state.metadata.status : undefined;
|
|
77
|
+
if (metadataStatus === "error" || metadataStatus === "cancel" || metadataStatus === "cancelled" ||
|
|
78
|
+
metadataStatus === "canceled" || metadataStatus === "interrupted")
|
|
79
|
+
return null;
|
|
80
|
+
const metadataExit = record(state.metadata) ? state.metadata.exit : undefined;
|
|
81
|
+
const output = nativeToolText(state.output) ?? nativeToolText(state.content);
|
|
82
|
+
const match = output?.match(/^exit_code=(0|[1-9]\d*|-[1-9]\d*|unknown)(?:\r?\n|$)/u);
|
|
83
|
+
const outputExit = match && match[1] !== "unknown" ? Number(match[1]) : null;
|
|
84
|
+
const explicit = typeof metadataExit === "number" && Number.isSafeInteger(metadataExit) ? metadataExit : undefined;
|
|
85
|
+
if (explicit !== undefined && match && explicit !== outputExit)
|
|
86
|
+
return null;
|
|
87
|
+
return explicit ?? outputExit;
|
|
88
|
+
}
|
|
63
89
|
/** Show native command outcomes to the Reviewer without turning non-criterion checks into acceptance evidence. */
|
|
64
90
|
export function observedMissionValidation(validation, childSessionID, history, notBefore, directory, directories) {
|
|
65
91
|
const declared = validation.map(normalizeCommand);
|
|
@@ -69,27 +95,53 @@ export function observedMissionValidation(validation, childSessionID, history, n
|
|
|
69
95
|
!Array.isArray(message.parts))
|
|
70
96
|
return [];
|
|
71
97
|
return message.parts.flatMap(part => {
|
|
72
|
-
if (!record(part) || part.type !== "tool" || !
|
|
73
|
-
!record(part.state) || !["completed", "error"].includes(String(part.state.status)) || !record(part.state.input) ||
|
|
74
|
-
typeof part.state.input.command !== "string")
|
|
98
|
+
if (!record(part) || part.type !== "tool" || !record(part.state) || !["completed", "error"].includes(String(part.state.status)))
|
|
75
99
|
return [];
|
|
76
|
-
// Match raw input first: whitespace normalization destroys Python heredoc boundaries.
|
|
77
|
-
const command = part.state.input.command;
|
|
78
|
-
const exit = record(part.state.metadata) ? part.state.metadata.exit : undefined;
|
|
79
100
|
const started = record(part.time) ? time(part.time.ran) : record(part.state.time) ? time(part.state.time.start) : null;
|
|
80
101
|
const completed = record(part.time) ? time(part.time.completed) : record(part.state.time) ? time(part.state.time.end) : null;
|
|
81
102
|
if (notBefore !== undefined && (started === null || completed === null || started < notBefore || completed < started))
|
|
82
103
|
return [];
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
if (["bash", "shell", "powershell", "pwsh"].includes(String(part.tool))) {
|
|
105
|
+
if (!record(part.state.input) || typeof part.state.input.command !== "string")
|
|
106
|
+
return [];
|
|
107
|
+
// Match raw input first: whitespace normalization destroys Python heredoc boundaries.
|
|
108
|
+
const exit = record(part.state.metadata) ? part.state.metadata.exit : undefined;
|
|
109
|
+
return [{ command: part.state.input.command,
|
|
110
|
+
directory: typeof part.state.input.workdir === "string" ? part.state.input.workdir : undefined,
|
|
111
|
+
exit_code: typeof exit === "number" && Number.isSafeInteger(exit) ? exit : null,
|
|
112
|
+
started_ms: started, completed_ms: completed }];
|
|
113
|
+
}
|
|
114
|
+
if ((part.tool === "fh_docker_exec" || part.tool === "fh.docker_exec") && record(part.state.input) &&
|
|
115
|
+
typeof part.state.input.command === "string") {
|
|
116
|
+
const input = part.state.input;
|
|
117
|
+
return [{ command: part.state.input.command,
|
|
118
|
+
...(isDockerWorkspacePath(input.cwd) ? { directory: input.cwd } : { directory_status: "unobserved" }),
|
|
119
|
+
docker: true, exit_code: dockerExecExitCode(part.state),
|
|
120
|
+
started_ms: started, completed_ms: completed }];
|
|
121
|
+
}
|
|
122
|
+
// Codemode's outer `execute` record retains the real namespaced call in toolCalls,
|
|
123
|
+
// while its result carries the Docker plugin's explicit exit_code header. Only a
|
|
124
|
+
// single completed Docker call can be paired unambiguously with that result.
|
|
125
|
+
if (part.tool !== "execute" || part.state.status !== "completed" || !record(part.state.metadata) ||
|
|
126
|
+
!Array.isArray(part.state.metadata.toolCalls) || part.state.metadata.toolCalls.length !== 1)
|
|
127
|
+
return [];
|
|
128
|
+
const [call] = part.state.metadata.toolCalls;
|
|
129
|
+
if (!record(call) || call.tool !== "fh.docker_exec" || call.status !== "completed" ||
|
|
130
|
+
!record(call.input) || typeof call.input.command !== "string")
|
|
131
|
+
return [];
|
|
132
|
+
return [{ command: call.input.command,
|
|
133
|
+
...(isDockerWorkspacePath(call.input.cwd) ? { directory: call.input.cwd } : { directory_status: "unobserved" }),
|
|
134
|
+
docker: true,
|
|
135
|
+
exit_code: dockerExecExitCode(part.state), started_ms: started, completed_ms: completed }];
|
|
86
136
|
});
|
|
87
137
|
});
|
|
88
138
|
let occurrence = 0;
|
|
89
139
|
const attempts = rawAttempts.sort((a, b) => (a.started_ms ?? 0) - (b.started_ms ?? 0) || (a.completed_ms ?? 0) - (b.completed_ms ?? 0)).flatMap(attempt => {
|
|
90
140
|
const exact = canonicalDeclaredValidationMembers(attempt.command, declared, occurrence);
|
|
91
141
|
const members = exact ?? (attempt.exit_code === 0 ? canonicalObservedValidationMembers(attempt.command, declared, occurrence) : undefined);
|
|
92
|
-
|
|
142
|
+
const directoryMatches = attempt.directory_status === "unobserved" ? directory === undefined
|
|
143
|
+
: validationDirectoryMatches(members ?? [], attempt.directory, directory, directories);
|
|
144
|
+
if (!members || !directoryMatches)
|
|
93
145
|
return [];
|
|
94
146
|
if (attempt.exit_code !== 0)
|
|
95
147
|
occurrence = 0;
|
|
@@ -100,9 +152,10 @@ export function observedMissionValidation(validation, childSessionID, history, n
|
|
|
100
152
|
if (command === declared[occurrence])
|
|
101
153
|
occurrence = (occurrence + 1) % declared.length;
|
|
102
154
|
}
|
|
103
|
-
const { directory: actual, ...visible } = attempt;
|
|
104
|
-
return [{ ...visible, command: members.join(" && "), ...(!exact ? { native_command: attempt.command } : {}),
|
|
105
|
-
|
|
155
|
+
const { directory: actual, docker, ...visible } = attempt;
|
|
156
|
+
return [{ ...visible, command: members.join(" && "), ...(!exact ? { native_command: attempt.command } : {}),
|
|
157
|
+
...(actual && (docker || directory && resolve(directory, actual) !== resolve(directory))
|
|
158
|
+
? { directory: docker ? actual : resolve(directory, actual) } : {}), ...(members.length > 1 ? { observed_members: members } : {}) }];
|
|
106
159
|
});
|
|
107
160
|
// Retain early attempts and the latest checks if a Worker retried many times.
|
|
108
161
|
const shown = attempts.length > 32 ? [...attempts.slice(0, 8), ...attempts.slice(-24)] : attempts;
|
|
@@ -131,6 +184,23 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
|
|
|
131
184
|
editedAt = Math.max(editedAt, end);
|
|
132
185
|
continue;
|
|
133
186
|
}
|
|
187
|
+
if (record(part) && part.type === "tool" && (part.tool === "fh_docker_exec" || part.tool === "fh.docker_exec") &&
|
|
188
|
+
record(part.state) && record(part.state.input) && typeof part.state.input.command === "string" &&
|
|
189
|
+
isDockerContainerID(part.state.input.container_id) && isDockerWorkspacePath(part.state.input.cwd)) {
|
|
190
|
+
const input = part.state.input, command = input.command, containerID = input.container_id, cwd = input.cwd;
|
|
191
|
+
const callID = typeof part.callID === "string" ? part.callID
|
|
192
|
+
: typeof part.id === "string" ? part.id : undefined;
|
|
193
|
+
const timing = record(part.time) ? { started: part.time.ran, completed: part.time.completed }
|
|
194
|
+
: record(part.state.time) ? { started: part.state.time.start, completed: part.state.time.end } : undefined;
|
|
195
|
+
if (!callID || seen.has(callID) || !timing || typeof timing.started !== "number" || typeof timing.completed !== "number" ||
|
|
196
|
+
!Number.isFinite(timing.started) || !Number.isFinite(timing.completed) || timing.started < notBefore || timing.completed < timing.started)
|
|
197
|
+
continue;
|
|
198
|
+
seen.add(callID);
|
|
199
|
+
attempts.push({ callID, command: command, directory: cwd,
|
|
200
|
+
dockerWorkspace: { container_id: containerID, cwd: cwd }, exit: dockerExecExitCode(part.state),
|
|
201
|
+
started: timing.started, completed: timing.completed });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
134
204
|
if (!record(part) || part.type !== "tool" || !["bash", "shell", "powershell", "pwsh"].includes(String(part.tool)) ||
|
|
135
205
|
!record(part.state) || !["completed", "error"].includes(String(part.state.status)) ||
|
|
136
206
|
!record(part.state.input) || typeof part.state.input.command !== "string")
|
|
@@ -153,6 +223,28 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
|
|
|
153
223
|
}
|
|
154
224
|
}
|
|
155
225
|
const matched = [];
|
|
226
|
+
const checkDirectoryMatches = (check, attempt, command) => {
|
|
227
|
+
if (attempt.dockerWorkspace) {
|
|
228
|
+
const workspace = check.binding.docker_workspace;
|
|
229
|
+
return workspace?.container_id === attempt.dockerWorkspace.container_id && workspace.cwd === attempt.dockerWorkspace.cwd &&
|
|
230
|
+
check.directory === attempt.dockerWorkspace.cwd && validationDirectoryMatches([command], check.directory, directory, directories);
|
|
231
|
+
}
|
|
232
|
+
return check.binding?.docker_workspace === undefined && validationDirectoryMatches([command], check.directory, directory, directories);
|
|
233
|
+
};
|
|
234
|
+
// Reuse the completed-sequence binding predicate before retaining an incomplete
|
|
235
|
+
// prefix too; an exit-zero call with an invalid saved check is not reusable proof.
|
|
236
|
+
const bindingUnavailable = (item) => {
|
|
237
|
+
if (!checks)
|
|
238
|
+
return false;
|
|
239
|
+
const command = declared[item.occurrence];
|
|
240
|
+
const check = checks.find(check => check.callID === item.callID && check.childSessionID === child);
|
|
241
|
+
const attempt = attempts.find(attempt => attempt.callID === item.callID);
|
|
242
|
+
return !check || !attempt || !check.fresh || check.exitCode !== 0 || check.command[item.member] !== command ||
|
|
243
|
+
!checkDirectoryMatches(check, attempt, command) ||
|
|
244
|
+
directory !== undefined && resolve(directory, check.directory ?? ".") !== resolve(directory, attempt.directory ?? ".") ||
|
|
245
|
+
!Number.isFinite(Date.parse(check.startedAt)) || !Number.isFinite(Date.parse(check.endedAt)) ||
|
|
246
|
+
Date.parse(check.startedAt) < notBefore || Date.parse(check.endedAt) < Date.parse(check.startedAt);
|
|
247
|
+
};
|
|
156
248
|
// Registration repairs replace only affected recipes. Retain the ordered actual
|
|
157
249
|
// occurrences of unchanged checks and join the changed checks after their own
|
|
158
250
|
// registration, rather than making an inserted/corrected command rerun its neighbors.
|
|
@@ -185,7 +277,11 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
|
|
|
185
277
|
if (!commands || registeredAt && recorded && JSON.stringify(commands) !== JSON.stringify(recorded.command))
|
|
186
278
|
continue;
|
|
187
279
|
const members = commands.flatMap((command, member) => required.includes(command) &&
|
|
188
|
-
|
|
280
|
+
(attempt.dockerWorkspace
|
|
281
|
+
? recorded?.binding?.docker_workspace?.container_id === attempt.dockerWorkspace.container_id &&
|
|
282
|
+
recorded.binding?.docker_workspace?.cwd === attempt.dockerWorkspace.cwd && recorded.directory === attempt.dockerWorkspace.cwd &&
|
|
283
|
+
validationDirectoryMatches([command], attempt.directory, directory, directories)
|
|
284
|
+
: recorded?.binding?.docker_workspace === undefined && validationDirectoryMatches([command], attempt.directory, directory, directories)) &&
|
|
189
285
|
(registeredAt?.[command] === undefined || Number.isFinite(Date.parse(registeredAt[command])) &&
|
|
190
286
|
attempt.started >= Date.parse(registeredAt[command])) ? [{ command, member }] : []);
|
|
191
287
|
if (!members.length || !registeredAt && members.length !== commands.length)
|
|
@@ -212,8 +308,16 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
|
|
|
212
308
|
failed = undefined;
|
|
213
309
|
// A failed && chain proves no member, even if earlier members could have passed.
|
|
214
310
|
if (attempt.exit !== 0) {
|
|
311
|
+
// A standalone, unique final check can be retried after setup repair without
|
|
312
|
+
// replaying its passed prefix. It still fails until an actual successful retry;
|
|
313
|
+
// the retained prefix and replacement tail must pass the usual binding/freshness checks.
|
|
314
|
+
const retryableTail = commands.length === 1 && members.length === 1 &&
|
|
315
|
+
partial.length === required.length - 1 && members[0].command === required.at(-1) &&
|
|
316
|
+
required.indexOf(members[0].command) === required.length - 1 &&
|
|
317
|
+
partial.every(item => !bindingUnavailable(item));
|
|
215
318
|
completed = undefined;
|
|
216
|
-
|
|
319
|
+
if (!retryableTail)
|
|
320
|
+
partial.length = 0;
|
|
217
321
|
failed = { command: commands, outcome: "fail", exitCode: attempt.exit };
|
|
218
322
|
continue;
|
|
219
323
|
}
|
|
@@ -231,7 +335,8 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
|
|
|
231
335
|
}
|
|
232
336
|
}
|
|
233
337
|
if (failed)
|
|
234
|
-
return { ready: false, reason: `mission-review-correction-validation-failed:${failed.command.join(" && ")}`, failure: failed,
|
|
338
|
+
return { ready: false, reason: `mission-review-correction-validation-failed:${failed.command.join(" && ")}`, failure: failed,
|
|
339
|
+
nextOccurrence: occurrences[partial.length] ?? occurrences[0] };
|
|
235
340
|
if (!completed) {
|
|
236
341
|
const next = occurrences[partial.length] ?? occurrences[0];
|
|
237
342
|
if (!incomplete || next < incomplete.nextOccurrence)
|
|
@@ -248,13 +353,7 @@ export function reviewerCorrectionValidation(validation, child, history, notBefo
|
|
|
248
353
|
complete.sort((a, b) => a.occurrence - b.occurrence);
|
|
249
354
|
for (const item of complete) {
|
|
250
355
|
const command = declared[item.occurrence];
|
|
251
|
-
|
|
252
|
-
const attempt = attempts.find(attempt => attempt.callID === item.callID);
|
|
253
|
-
if (checks && (!check || !check.fresh || check.exitCode !== 0 || check.command[item.member] !== command ||
|
|
254
|
-
!validationDirectoryMatches([command], check.directory, directory, directories) ||
|
|
255
|
-
directory !== undefined && resolve(directory, check.directory ?? ".") !== resolve(directory, attempt.directory ?? ".") ||
|
|
256
|
-
!Number.isFinite(Date.parse(check.startedAt)) || !Number.isFinite(Date.parse(check.endedAt)) ||
|
|
257
|
-
Date.parse(check.startedAt) < notBefore || Date.parse(check.endedAt) < Date.parse(check.startedAt))) {
|
|
356
|
+
if (bindingUnavailable(item)) {
|
|
258
357
|
return { ready: false, reason: `mission-review-correction-validation-binding-unavailable:${command}` };
|
|
259
358
|
}
|
|
260
359
|
}
|
|
@@ -299,10 +398,11 @@ export async function observedMissionValidationSummary(validation, childSessionI
|
|
|
299
398
|
const observed = observedMissionValidation(validation, childSessionID, data.filter(record), notBefore, directory, directories);
|
|
300
399
|
const grouped = new Map();
|
|
301
400
|
for (const attempt of observed.attempts) {
|
|
302
|
-
const key = JSON.stringify([attempt.command, attempt.native_command, attempt.directory, attempt.exit_code]);
|
|
401
|
+
const key = JSON.stringify([attempt.command, attempt.native_command, attempt.directory, attempt.directory_status, attempt.exit_code]);
|
|
303
402
|
const previous = grouped.get(key);
|
|
304
403
|
grouped.set(key, { command: attempt.command, ...(attempt.native_command ? { native_command: attempt.native_command } : {}),
|
|
305
|
-
...(attempt.directory ? { directory: attempt.directory } : {}),
|
|
404
|
+
...(attempt.directory ? { directory: attempt.directory } : {}),
|
|
405
|
+
...(attempt.directory_status ? { directory_status: attempt.directory_status } : {}), exit_code: attempt.exit_code,
|
|
306
406
|
observed_attempts: (previous?.observed_attempts ?? 0) + 1,
|
|
307
407
|
latest_started_ms: attempt.started_ms, latest_completed_ms: attempt.completed_ms });
|
|
308
408
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Native V2 truncates at whole-line boundaries.
|
|
3
|
-
*
|
|
4
|
-
* recipes, with one top-level field per line; keep the complete JSON unchanged.
|
|
2
|
+
* Native V2 truncates at whole-line boundaries. Put actionable facts and bounded
|
|
3
|
+
* model observations before bulky history/proof; preserve every original value.
|
|
5
4
|
*/
|
|
6
5
|
export declare function serializeOperatorStatus(packet: Record<string, unknown>): string;
|
|
@@ -1,18 +1,53 @@
|
|
|
1
|
+
const RECENT_ATTEMPTS = 8;
|
|
2
|
+
const RECENT_MODEL_ATTEMPTS = 8;
|
|
3
|
+
const ATTEMPT_FIELDS = ["attemptID", "runID", "unitID", "kind", "childSessionID", "status", "nativeOutcome",
|
|
4
|
+
"selectedModel", "observedModel", "observedVariant", "resultClass"];
|
|
5
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
/** Display-only projection: never infer an observed model from a selection. */
|
|
7
|
+
function recentModelObservations(attempts) {
|
|
8
|
+
const selected = new Set();
|
|
9
|
+
for (let index = Math.max(0, attempts.length - RECENT_ATTEMPTS); index < attempts.length; index++)
|
|
10
|
+
selected.add(index);
|
|
11
|
+
// Model-less direct work must not displace the most recent model observations.
|
|
12
|
+
let models = 0;
|
|
13
|
+
for (let index = attempts.length - 1; index >= 0 && models < RECENT_MODEL_ATTEMPTS; index--) {
|
|
14
|
+
const attempt = attempts[index];
|
|
15
|
+
if (record(attempt) && [attempt.selectedModel, attempt.observedModel].some(value => typeof value === "string" && value.length > 0)) {
|
|
16
|
+
selected.add(index);
|
|
17
|
+
models++;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const recent = [...selected].sort((left, right) => left - right).flatMap(index => {
|
|
21
|
+
const attempt = attempts[index];
|
|
22
|
+
if (!record(attempt))
|
|
23
|
+
return [];
|
|
24
|
+
const summary = Object.fromEntries(ATTEMPT_FIELDS.filter(key => Object.hasOwn(attempt, key)).map(key => [key, attempt[key]]));
|
|
25
|
+
if (record(attempt.failure)) {
|
|
26
|
+
const failure = attempt.failure;
|
|
27
|
+
summary.failure = Object.fromEntries(["category", "code"].filter(key => Object.hasOwn(failure, key)).map(key => [key, failure[key]]));
|
|
28
|
+
}
|
|
29
|
+
else if (attempt.failure === null)
|
|
30
|
+
summary.failure = null;
|
|
31
|
+
return [summary];
|
|
32
|
+
});
|
|
33
|
+
return { recent_attempts: recent, omitted_attempts: attempts.length - recent.length };
|
|
34
|
+
}
|
|
1
35
|
/**
|
|
2
|
-
* Native V2 truncates at whole-line boundaries.
|
|
3
|
-
*
|
|
4
|
-
* recipes, with one top-level field per line; keep the complete JSON unchanged.
|
|
36
|
+
* Native V2 truncates at whole-line boundaries. Put actionable facts and bounded
|
|
37
|
+
* model observations before bulky history/proof; preserve every original value.
|
|
5
38
|
*/
|
|
6
39
|
export function serializeOperatorStatus(packet) {
|
|
40
|
+
const display = !Object.hasOwn(packet, "model_observations") && Array.isArray(packet.attempts) && packet.attempts.length > 0
|
|
41
|
+
? { ...packet, model_observations: recentModelObservations(packet.attempts) } : packet;
|
|
7
42
|
const keys = new Set([
|
|
8
43
|
"profile", "view", "mission_id", "run_id", "phase", "status", "decision", "coordinator_session_id",
|
|
9
|
-
"work_quantity", "budget", "accounting_scope", "
|
|
10
|
-
...Object.keys(
|
|
44
|
+
"work_quantity", "budget", "accounting_scope", "next_action", "execution_summary", "runtime", "model_observations", "completion", "attempts",
|
|
45
|
+
...Object.keys(display),
|
|
11
46
|
]);
|
|
12
47
|
const lines = [...keys].flatMap(key => {
|
|
13
|
-
if (!Object.hasOwn(
|
|
48
|
+
if (!Object.hasOwn(display, key))
|
|
14
49
|
return [];
|
|
15
|
-
const value = JSON.stringify(
|
|
50
|
+
const value = JSON.stringify(display[key]);
|
|
16
51
|
return value === undefined ? [] : [` ${JSON.stringify(key)}: ${value}`];
|
|
17
52
|
});
|
|
18
53
|
return `{\n${lines.join(",\n")}\n}`;
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -239,8 +239,10 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
239
239
|
if (!child)
|
|
240
240
|
throw new Error("native-worker-history-session-unavailable");
|
|
241
241
|
const direct = await directExecution(child) ?? await reviewerCorrection(child);
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
const method = typeof nativeSession?.reviewMessages === "function" ? "reviewMessages" : "messages";
|
|
243
|
+
return observedMissionValidationSummary(validation, child, typeof nativeSession?.[method] === "function"
|
|
244
|
+
? () => session(method, { path: { id: child }, query: { directory: input.directory,
|
|
245
|
+
...(notBefore === undefined ? {} : { notBefore: new Date(notBefore).toISOString() }) } }) : undefined, notBefore, notBefore === undefined ? undefined : input.directory, direct?.unit.directExecution?.validationCwd ?? direct?.unit.unit.validation_cwd);
|
|
244
246
|
}
|
|
245
247
|
async function reviewMessages(id, notBefore, requireAvailable = false) {
|
|
246
248
|
const method = typeof nativeSession?.reviewMessages === "function" ? "reviewMessages" : "messages";
|
|
@@ -679,7 +681,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
679
681
|
return undefined;
|
|
680
682
|
const unit = correction.unit;
|
|
681
683
|
const directories = unit.directExecution?.validationCwd ?? unit.unit.validation_cwd;
|
|
682
|
-
const
|
|
684
|
+
const admittedAt = unit.reviewerCorrection?.admittedAt ?? unit.directExecution?.startedAt ?? correction.run.createdAt;
|
|
685
|
+
const progress = reviewerCorrectionValidation(unit.unit.validation, child, await reviewMessages(child, admittedAt), Date.parse(admittedAt), unit.reviewerCorrection?.checks ?? unit.directExecution?.checks ?? [], input.directory, directories, unit.reviewerCorrection?.validationRegisteredAt ?? unit.directExecution?.validationRegisteredAt);
|
|
683
686
|
const members = canonicalObservedValidationMembers(command, unit.unit.validation, progress.nextOccurrence ?? 0);
|
|
684
687
|
return members && validationDirectoryMatches(members, directory, input.directory, directories) ? members : undefined;
|
|
685
688
|
},
|
|
@@ -697,8 +700,10 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
697
700
|
(item.reviewerCorrection?.author === child || item.directExecution?.actor === child));
|
|
698
701
|
if (!unit)
|
|
699
702
|
return undefined;
|
|
703
|
+
// Compaction removes earlier tools from session.context, not from native history.
|
|
704
|
+
// Reuse the admission-window reader; saved checks still require their live input binding.
|
|
700
705
|
try {
|
|
701
|
-
return await reviewerCorrectionValidationFresh(unit.unit.validation, child, await
|
|
706
|
+
return await reviewerCorrectionValidationFresh(unit.unit.validation, child, await reviewMessages(child, new Date(startedAt).toISOString()), startedAt, unit.reviewerCorrection?.checks ?? unit.directExecution?.checks ?? [], input.directory, unit.directExecution?.validationCwd ?? unit.unit.validation_cwd, unit.reviewerCorrection?.validationRegisteredAt ?? unit.directExecution?.validationRegisteredAt);
|
|
702
707
|
}
|
|
703
708
|
catch {
|
|
704
709
|
return { ready: false, reason: "mission-review-correction-validation-history-unavailable" };
|
|
@@ -3515,7 +3520,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3515
3520
|
for (const unit of run.units) {
|
|
3516
3521
|
if (!unit.reviewerCorrection || !unit.childSessionID)
|
|
3517
3522
|
continue;
|
|
3518
|
-
const
|
|
3523
|
+
const admittedAt = unit.reviewerCorrection.admittedAt ?? run.createdAt;
|
|
3524
|
+
const checked = await reviewerCorrectionValidationFresh(unit.unit.validation, unit.childSessionID, await reviewMessages(unit.childSessionID, admittedAt), Date.parse(admittedAt), unit.reviewerCorrection.checks ?? [], input.directory, unit.directExecution?.validationCwd ?? unit.unit.validation_cwd, unit.reviewerCorrection.validationRegisteredAt);
|
|
3519
3525
|
if (!checked.ready)
|
|
3520
3526
|
return checked.reason;
|
|
3521
3527
|
}
|
|
@@ -3529,7 +3535,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3529
3535
|
async function observeNativeSelfRecheck(root, mission, run, author, callID, promptID, admittedAt, expectedSource, suppliedHistory) {
|
|
3530
3536
|
if (!promptID || !Number.isFinite(admittedAt) || mission.runID !== run.runID || run.phase !== "awaiting-acceptance")
|
|
3531
3537
|
return undefined;
|
|
3532
|
-
const history = suppliedHistory ?? await
|
|
3538
|
+
const history = suppliedHistory ?? await reviewMessages(author, new Date(admittedAt).toISOString());
|
|
3533
3539
|
const prompt = history.findIndex(message => record(message.info) && message.info.id === promptID && message.info.role === "user" && message.info.sessionID === author);
|
|
3534
3540
|
if (prompt < 0)
|
|
3535
3541
|
return undefined;
|
|
@@ -53,6 +53,16 @@ export declare function protectedSnapshot(authorization: {
|
|
|
53
53
|
readonly source: string;
|
|
54
54
|
readonly candidate: string;
|
|
55
55
|
} | undefined>;
|
|
56
|
+
/** Capture a task workspace that exists only inside the fixed Docker task container. */
|
|
57
|
+
export declare function protectedDockerSnapshot(authorization: {
|
|
58
|
+
manifestPath: string;
|
|
59
|
+
manifestHash: string;
|
|
60
|
+
projectRoot: string;
|
|
61
|
+
}, containerID: string, cwd: string): Promise<{
|
|
62
|
+
readonly binding: Binding;
|
|
63
|
+
readonly source: string;
|
|
64
|
+
readonly candidate: string;
|
|
65
|
+
} | undefined>;
|
|
56
66
|
export declare function refreshProtectedSnapshot(projectRoot: string, binding: Binding): Promise<{
|
|
57
67
|
readonly source: string;
|
|
58
68
|
readonly candidate: string;
|
|
@@ -7,6 +7,7 @@ import { normalizeManifestScope } from "../core/path.js";
|
|
|
7
7
|
import { RUNTIME_PROFILES } from "../core/runtime-profile.js";
|
|
8
8
|
import { declaredArtifacts } from "./declared-artifacts.js";
|
|
9
9
|
import { validationScratchPaths } from "./validation-scratch.js";
|
|
10
|
+
import { captureDockerWorkspaceSnapshot } from "./docker-validation.js";
|
|
10
11
|
import { execFile } from "node:child_process";
|
|
11
12
|
import { promisify } from "node:util";
|
|
12
13
|
const controlRoots = new Set([".git", ...Object.values(RUNTIME_PROFILES).map(profile => profile.stateDirectory)]);
|
|
@@ -505,7 +506,34 @@ export async function protectedSnapshot(authorization, options = {}) {
|
|
|
505
506
|
source_paths: sourcePaths.map(path => evidencePath(path).replaceAll("\\", "/")),
|
|
506
507
|
candidate_paths: candidatePaths.map(path => evidencePath(path).replaceAll("\\", "/")) }, source, candidate };
|
|
507
508
|
}
|
|
509
|
+
/** Capture a task workspace that exists only inside the fixed Docker task container. */
|
|
510
|
+
export async function protectedDockerSnapshot(authorization, containerID, cwd) {
|
|
511
|
+
const manifestSource = await readFile(authorization.manifestPath).catch(() => undefined);
|
|
512
|
+
if (manifestSource === undefined)
|
|
513
|
+
return undefined;
|
|
514
|
+
const manifestHash = createHash("sha256").update(manifestSource).digest("hex");
|
|
515
|
+
if (manifestHash !== authorization.manifestHash)
|
|
516
|
+
return undefined;
|
|
517
|
+
const relativePath = relative(authorization.projectRoot, authorization.manifestPath).replaceAll("\\", "/");
|
|
518
|
+
if (relativePath.startsWith("../") || isAbsolute(relativePath))
|
|
519
|
+
return undefined;
|
|
520
|
+
const workspace = await captureDockerWorkspaceSnapshot(containerID, cwd);
|
|
521
|
+
if (!workspace)
|
|
522
|
+
return undefined;
|
|
523
|
+
return { binding: { manifest_hash: `sha256:${manifestHash}`, project_root: authorization.projectRoot,
|
|
524
|
+
manifest_path: relativePath, source_policy: "declared-paths-v1", source_paths: [], candidate_paths: [],
|
|
525
|
+
docker_workspace: workspace }, source: workspace.source, candidate: workspace.candidate };
|
|
526
|
+
}
|
|
508
527
|
export async function refreshProtectedSnapshot(projectRoot, binding) {
|
|
528
|
+
if (binding.docker_workspace) {
|
|
529
|
+
if (resolve(projectRoot) !== resolve(binding.project_root) || !await snapshotManifest(projectRoot, binding))
|
|
530
|
+
return undefined;
|
|
531
|
+
const workspace = await captureDockerWorkspaceSnapshot(binding.docker_workspace.container_id, binding.docker_workspace.cwd);
|
|
532
|
+
if (!workspace || workspace.container_id !== binding.docker_workspace.container_id ||
|
|
533
|
+
workspace.cwd !== binding.docker_workspace.cwd || workspace.repository_root !== binding.docker_workspace.repository_root)
|
|
534
|
+
return undefined;
|
|
535
|
+
return { source: workspace.source, candidate: workspace.candidate };
|
|
536
|
+
}
|
|
509
537
|
const current = await snapshotManifest(projectRoot, binding);
|
|
510
538
|
if (!current)
|
|
511
539
|
return undefined;
|
package/dist/plugin/v2.d.ts
CHANGED
|
@@ -1,4 +1,77 @@
|
|
|
1
1
|
import type { OpenCodeHooks, OpenCodePlugin } from "./index.js";
|
|
2
|
+
export declare const V010_AGENT_TOOL_REGISTRY: {
|
|
3
|
+
schema_version: number;
|
|
4
|
+
tool_prefix: string;
|
|
5
|
+
core_tools: string[];
|
|
6
|
+
runtime_agents: {
|
|
7
|
+
"dog-coordinator": string;
|
|
8
|
+
"dog-worker": string;
|
|
9
|
+
"dog-luna-worker": string;
|
|
10
|
+
"dog-scout": string;
|
|
11
|
+
"dog-reviewer": string;
|
|
12
|
+
"dog-advisor": string;
|
|
13
|
+
"dog-operator": string;
|
|
14
|
+
};
|
|
15
|
+
roles: {
|
|
16
|
+
"dog-coordinator": {
|
|
17
|
+
tool_families: {
|
|
18
|
+
mission_lifecycle: string[];
|
|
19
|
+
unit_execution: string[];
|
|
20
|
+
review: string[];
|
|
21
|
+
};
|
|
22
|
+
children: string[];
|
|
23
|
+
};
|
|
24
|
+
"dog-worker": {
|
|
25
|
+
tool_families: {
|
|
26
|
+
workspace: string[];
|
|
27
|
+
};
|
|
28
|
+
native_tool_families: {
|
|
29
|
+
docker_task: string[];
|
|
30
|
+
};
|
|
31
|
+
core_tools: string[];
|
|
32
|
+
children: never[];
|
|
33
|
+
};
|
|
34
|
+
"dog-luna-worker": {
|
|
35
|
+
tool_families: {
|
|
36
|
+
workspace: string[];
|
|
37
|
+
};
|
|
38
|
+
core_tools: string[];
|
|
39
|
+
children: never[];
|
|
40
|
+
};
|
|
41
|
+
"dog-scout": {
|
|
42
|
+
tool_families: {};
|
|
43
|
+
children: never[];
|
|
44
|
+
};
|
|
45
|
+
"dog-reviewer": {
|
|
46
|
+
tool_families: {
|
|
47
|
+
review: string[];
|
|
48
|
+
};
|
|
49
|
+
native_tool_families: {
|
|
50
|
+
docker_review: string[];
|
|
51
|
+
};
|
|
52
|
+
correction_native_tool_families: {
|
|
53
|
+
docker_task: string[];
|
|
54
|
+
};
|
|
55
|
+
correction_tool_families: {
|
|
56
|
+
workspace: string[];
|
|
57
|
+
correction: string[];
|
|
58
|
+
};
|
|
59
|
+
children: never[];
|
|
60
|
+
};
|
|
61
|
+
"dog-advisor": {
|
|
62
|
+
tool_families: {};
|
|
63
|
+
children: never[];
|
|
64
|
+
};
|
|
65
|
+
"dog-operator": {
|
|
66
|
+
tool_families: {
|
|
67
|
+
mission_lifecycle: string[];
|
|
68
|
+
unit_execution: string[];
|
|
69
|
+
review: string[];
|
|
70
|
+
};
|
|
71
|
+
children: string[];
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
};
|
|
2
75
|
type JsonObject = Record<string, unknown>;
|
|
3
76
|
type NativePermissionRule = {
|
|
4
77
|
action: string;
|
package/dist/plugin/v2.js
CHANGED
|
@@ -15,6 +15,9 @@ import { modelLiveState } from "./model-live-state.js";
|
|
|
15
15
|
import { serializeOperatorStatus } from "./operator-status-output.js";
|
|
16
16
|
import { runtimeAssets as reviewerAssets } from "../runtime-assets-v010.js";
|
|
17
17
|
import { MISSION_RESULT_HOLD_POLICY, MISSION_REFLECTION_WORKFLOW, LEGACY_MISSION_REFLECTION_WORKFLOW } from "../runtime-mission-assets.js";
|
|
18
|
+
import agentToolRegistry from "./frontierharness-agent-tool-registry.json" with { type: "json" };
|
|
19
|
+
import { canonicalAgent, V010_RUNTIME_PROFILE } from "../core/runtime-profile.js";
|
|
20
|
+
export const V010_AGENT_TOOL_REGISTRY = agentToolRegistry;
|
|
18
21
|
// Capture once when this module evaluates. A later package replacement must not make an old
|
|
19
22
|
// process report the replacement's bytes as its loaded adapter.
|
|
20
23
|
const loadedAdapter = Object.freeze({ adapter_url: import.meta.url,
|
|
@@ -322,7 +325,7 @@ export function createV2ReturnReportFinalizer(context, hooks) {
|
|
|
322
325
|
}
|
|
323
326
|
const correctionAgent = (agent) => typeof agent === "string" && /^dog-reviewer-correction-v010-[a-f0-9]{16}$/u.test(agent);
|
|
324
327
|
const reviewerCorrectionOnlyTools = new Set(["edit", "write", "patch", "sortie_v010_bind_write_gate",
|
|
325
|
-
"sortie_v010_release_write_gate", "sortie_v010_operator_status", "sortie_v010_expand_unit", "sortie_v010_finish_direct_unit"]);
|
|
328
|
+
"sortie_v010_release_write_gate", "sortie_v010_operator_status", "sortie_v010_expand_unit", "sortie_v010_finish_direct_unit", "fh_docker_exec"]);
|
|
326
329
|
// Order only the genuine, already-filtered native definitions. Initial read-only
|
|
327
330
|
// tools remain a deterministic prefix when correction admits additional tools;
|
|
328
331
|
// object identities, schemas, executors and native authority are unchanged.
|
|
@@ -375,6 +378,29 @@ function shippedReviewerBlock(rules) {
|
|
|
375
378
|
}
|
|
376
379
|
return undefined;
|
|
377
380
|
}
|
|
381
|
+
function correctionGatewayReviewerRules(rules) {
|
|
382
|
+
const role = agentToolRegistry.roles["dog-reviewer"];
|
|
383
|
+
// Match product_agent_permissions()'s exact generated block, in place. A user's
|
|
384
|
+
// surrounding agent rules and later session rules retain native last-match order.
|
|
385
|
+
const generated = (correcting) => {
|
|
386
|
+
const actions = [...new Set([...Object.values(role.tool_families),
|
|
387
|
+
...(correcting ? Object.values(role.correction_tool_families) : [])].flat())].sort();
|
|
388
|
+
const native = [...new Set([...Object.values(role.native_tool_families),
|
|
389
|
+
...(correcting ? Object.values(role.correction_native_tool_families) : [])].flat())].sort();
|
|
390
|
+
return [{ action: "*", resource: "*", effect: "deny" },
|
|
391
|
+
...actions.map(action => ({ action: agentToolRegistry.tool_prefix + action, resource: "*", effect: "allow" })),
|
|
392
|
+
...native.map(action => ({ action, resource: "*", effect: "allow" }))];
|
|
393
|
+
};
|
|
394
|
+
const original = generated(false);
|
|
395
|
+
for (let start = rules.length - original.length; start >= 0; start--) {
|
|
396
|
+
if (original.every((rule, offset) => {
|
|
397
|
+
const actual = rules[start + offset];
|
|
398
|
+
return actual?.action === rule.action && actual.resource === rule.resource && actual.effect === rule.effect;
|
|
399
|
+
}))
|
|
400
|
+
return [...rules.slice(0, start), ...generated(true), ...rules.slice(start + original.length)];
|
|
401
|
+
}
|
|
402
|
+
return [...rules];
|
|
403
|
+
}
|
|
378
404
|
const correctionAgents = new WeakMap();
|
|
379
405
|
/** Use 2.0.18's agent.transform/switchAgent BEFORE native tool snapshot selection; never mutate session rules. */
|
|
380
406
|
export async function syncReviewerCorrectionPermissions(context, hooks, sessionID, activate = true) {
|
|
@@ -470,7 +496,7 @@ async function syncReviewerCorrectionPermissionsOnce(context, hooks, sessionID,
|
|
|
470
496
|
const reviewer = editor.get?.("dog-reviewer-v010");
|
|
471
497
|
if (!reviewer?.permissions)
|
|
472
498
|
throw new Error("native-reviewer-correction-role-permissions-unavailable");
|
|
473
|
-
const rules = reviewer.permissions;
|
|
499
|
+
const rules = correctionGatewayReviewerRules(reviewer.permissions);
|
|
474
500
|
const shipped = shippedReviewerBlock(rules);
|
|
475
501
|
if (!shipped)
|
|
476
502
|
throw new Error("native-reviewer-correction-readonly-role-block-unavailable");
|
|
@@ -479,8 +505,8 @@ async function syncReviewerCorrectionPermissionsOnce(context, hooks, sessionID,
|
|
|
479
505
|
...rules.slice(shipped.start + shipped.rules.length)];
|
|
480
506
|
editor.update(id, agent => Object.assign(agent, reviewer, { id, name: id, mode: "subagent", hidden: true,
|
|
481
507
|
model: { ...info.model },
|
|
482
|
-
//
|
|
483
|
-
//
|
|
508
|
+
// Project only the exact generated gateway block and shipped read-only edit/shell
|
|
509
|
+
// role entries. Other configured rules keep their native order/effective meaning.
|
|
484
510
|
// No edit/shell grants, copied historical denies, command scanner or session-rule replacement.
|
|
485
511
|
// The existing correction-control tools stay exposed beneath the shipped sortie_* deny.
|
|
486
512
|
// Common implementation gates own known output scope; the ordinary Reviewer is untouched.
|
|
@@ -792,21 +818,24 @@ async function registerV2Hooks(context, hooks, background, liveState) {
|
|
|
792
818
|
await hooks["experimental.chat.system.transform"]({ sessionID: String(event.sessionID ?? "") }, output);
|
|
793
819
|
await reconcileCorrectionPermissions(String(event.sessionID));
|
|
794
820
|
if (record(event.tools)) {
|
|
795
|
-
const visible = {
|
|
796
|
-
"dog-operator": ["start_mission", "plan_units", "start_direct_unit", "finish_direct_unit", "retry_mission_unit", "operator_next", "operator_status", "extend_mission_budget", "expand_unit", "review_mission", "repair_review", "complete_mission", "cancel_operator", "reflection"],
|
|
797
|
-
"dogs-coordinator": ["plan_units", "start_direct_unit", "finish_direct_unit", "operator_next", "operator_status", "expand_unit", "review_mission", "repair_review", "submit_mission", "skip_mission_consultation", "retry_mission_unit", "rescue_mission_unit"],
|
|
798
|
-
"dog-worker-v010": ["bind_write_gate", "release_write_gate", "operator_status", "expand_unit"],
|
|
799
|
-
"dog-luna-worker-v010": ["bind_write_gate", "release_write_gate", "operator_status", "expand_unit"],
|
|
800
|
-
"dog-reviewer-v010": await hooks.reviewerCorrectionScope?.(String(event.sessionID)) ? ["bind_write_gate", "release_write_gate", "operator_status", "expand_unit", "repair_review", "finish_direct_unit"] : ["repair_review"], "dog-scout-v010": [], "dog-advisor-v010": [],
|
|
801
|
-
};
|
|
802
821
|
const correcting = correctionAgent(event.agent) && !!await hooks.reviewerCorrectionScope?.(String(event.sessionID));
|
|
803
|
-
const
|
|
822
|
+
const logicalRole = canonicalAgent(V010_RUNTIME_PROFILE, String(event.agent));
|
|
823
|
+
const role = logicalRole ? agentToolRegistry.roles[logicalRole] : undefined;
|
|
824
|
+
const families = role ? [...Object.values(role.tool_families), ...(correcting ? Object.values(role.correction_tool_families ?? {}) : [])] : [];
|
|
825
|
+
const allowed = role ? [...new Set(families.flat())] : undefined;
|
|
826
|
+
const nativeFamilies = role ? [...Object.values(role.native_tool_families ?? {}),
|
|
827
|
+
...(correcting ? Object.values(role.correction_native_tool_families ?? {}) : [])] : [];
|
|
828
|
+
const allowedNativeTools = new Set(nativeFamilies.flat());
|
|
804
829
|
for (const key of Object.keys(event.tools)) {
|
|
805
830
|
if ((event.agent === "dog-reviewer-v010" || correctionAgent(event.agent)) && !correcting &&
|
|
806
831
|
["edit", "write", "patch"].includes(key)) {
|
|
807
832
|
delete event.tools[key];
|
|
808
833
|
continue;
|
|
809
834
|
}
|
|
835
|
+
if (key.startsWith("fh_") && !allowedNativeTools.has(key)) {
|
|
836
|
+
delete event.tools[key];
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
810
839
|
if (!key.startsWith("sortie_"))
|
|
811
840
|
continue;
|
|
812
841
|
if (!allowed || !key.startsWith("sortie_v010_") || !allowed.includes(key.slice("sortie_v010_".length)))
|