@hassangameryt/freeflow 0.8.0 → 0.9.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/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +96 -149
- package/capabilities/cognitive-routing/SKILL.md +21 -129
- package/capabilities/cognitive-routing/references/coordinator.md +163 -0
- package/capabilities/cognitive-routing/references/worker.md +38 -0
- package/capabilities/compaction/SKILL.md +3 -3
- package/capabilities/compaction/references/summary-format.md +26 -15
- package/capabilities/messaging/SKILL.md +49 -0
- package/capabilities/task-memory/SKILL.md +87 -0
- package/command-surface.json +6 -54
- package/gemini-extension.json +1 -1
- package/hooks/claude/hooks.json +13 -3
- package/hooks/codex/hooks.json +2 -1
- package/hooks/shared/runtime-context.mjs +69 -77
- package/package.json +25 -8
- package/pi-extension/dist/ask-user/ask.js +108 -0
- package/pi-extension/dist/ask-user/form.js +250 -0
- package/pi-extension/dist/ask-user/questions.js +66 -0
- package/pi-extension/dist/ask-user/tool.js +124 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
- package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
- package/pi-extension/dist/cognitive-routing/config.js +3 -4
- package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
- package/pi-extension/dist/cognitive-routing/gate.js +20 -5
- package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
- package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
- package/pi-extension/dist/cognitive-routing/projection.js +23 -6
- package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
- package/pi-extension/dist/cognitive-routing/render.js +15 -15
- package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
- package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
- package/pi-extension/dist/cognitive-routing/session.js +3 -1
- package/pi-extension/dist/cognitive-routing/sources.js +29 -4
- package/pi-extension/dist/cognitive-routing/state.js +18 -8
- package/pi-extension/dist/cognitive-routing/status.js +4 -3
- package/pi-extension/dist/cognitive-routing/tools.js +29 -15
- package/pi-extension/dist/cognitive-routing/types.js +10 -1
- package/pi-extension/dist/compaction/carry.js +36 -22
- package/pi-extension/dist/compaction/config.js +4 -2
- package/pi-extension/dist/compaction/controller.js +52 -29
- package/pi-extension/dist/compaction/harness.js +19 -3
- package/pi-extension/dist/compaction/results.js +14 -13
- package/pi-extension/dist/compaction/tool.js +6 -6
- package/pi-extension/dist/host/catalog.js +20 -35
- package/pi-extension/dist/host/config-validation.js +233 -0
- package/pi-extension/dist/host/config.js +78 -91
- package/pi-extension/dist/host/inbound.js +142 -0
- package/pi-extension/dist/host/projection-tags.js +1 -1
- package/pi-extension/dist/host/prompts.js +24 -13
- package/pi-extension/dist/host/read-only-session.js +6 -1
- package/pi-extension/dist/host/request-history.js +7 -3
- package/pi-extension/dist/host/rows.js +25 -0
- package/pi-extension/dist/host/runtime-state.js +17 -2
- package/pi-extension/dist/host/section-keeper.js +85 -0
- package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
- package/pi-extension/dist/host/settings/settings-view.js +27 -8
- package/pi-extension/dist/host/status.js +19 -5
- package/pi-extension/dist/index.js +638 -72
- package/pi-extension/dist/messaging/config.js +27 -0
- package/pi-extension/dist/messaging/messaging.js +398 -0
- package/pi-extension/dist/messaging/registry.js +57 -0
- package/pi-extension/dist/messaging/render.js +90 -0
- package/pi-extension/dist/messaging/tools.js +109 -0
- package/pi-extension/dist/messaging/transport.js +101 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
- package/pi-extension/dist/task-memory/config.js +49 -0
- package/pi-extension/dist/task-memory/registry.js +119 -0
- package/pi-extension/dist/task-memory/render.js +107 -0
- package/pi-extension/dist/task-memory/runtime.js +994 -0
- package/pi-extension/dist/task-memory/tool.js +233 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
- package/pi-extension/dist/tool-execution/background.js +220 -28
- package/pi-extension/dist/tool-execution/config.js +14 -3
- package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
- package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
- package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
- package/pi-extension/dist/tool-execution/tools.js +2 -2
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +24 -14
- package/runtime/prompts/interaction-contract.md +7 -4
- package/runtime/prompts/tool-execution.md +3 -16
- package/runtime/prompts/working-method.md +16 -99
- package/skills/deliver-work/SKILL.md +76 -0
- package/skills/deliver-work/references/branch-closeout.md +134 -0
- package/skills/deliver-work/references/commit.md +83 -0
- package/skills/deliver-work/references/launch.md +94 -0
- package/skills/deliver-work/references/release.md +85 -0
- package/skills/deliver-work/scripts/commit-state.mjs +139 -0
- package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
- package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
- package/skills/deliver-work/scripts/push-route.mjs +122 -0
- package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
- package/skills/discuss-work/SKILL.md +148 -0
- package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
- package/skills/discuss-work/references/learning-action.md +36 -0
- package/skills/discuss-work/scripts/choices.mjs +101 -0
- package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
- package/skills/evaluate-behavior/SKILL.md +107 -0
- package/skills/evaluate-behavior/references/authoring.md +381 -0
- package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
- package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
- package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
- package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
- package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
- package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
- package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
- package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
- package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
- package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
- package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
- package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
- package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
- package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
- package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
- package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
- package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
- package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
- package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
- package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
- package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
- package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
- package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
- package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
- package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
- package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
- package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
- package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
- package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
- package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
- package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
- package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
- package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
- package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
- package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
- package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
- package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
- package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
- package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
- package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
- package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
- package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
- package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
- package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
- package/skills/execute-work/SKILL.md +96 -104
- package/skills/execute-work/references/code-practices.md +27 -51
- package/skills/execute-work/references/design.md +271 -0
- package/skills/execute-work/references/diagnose.md +117 -0
- package/skills/execute-work/references/migration.md +108 -0
- package/skills/execute-work/references/simplify.md +65 -0
- package/skills/execute-work/scripts/self-review.mjs +422 -0
- package/skills/execute-work/scripts/touch-map.mjs +290 -0
- package/skills/review-work/SKILL.md +89 -65
- package/skills/review-work/references/adjudication.md +94 -0
- package/skills/review-work/references/artifact.md +198 -0
- package/skills/review-work/references/evals-and-judges.md +66 -0
- package/skills/review-work/references/work.md +177 -0
- package/skills/review-work/scripts/check-brief.mjs +128 -0
- package/skills/review-work/scripts/review-packet.mjs +233 -0
- package/skills/setup-freeflow/SKILL.md +68 -17
- package/skills/track-work/SKILL.md +52 -20
- package/skills/track-work/references/handoff.md +190 -0
- package/skills/track-work/references/projects.md +51 -0
- package/skills/track-work/references/working-record-format.md +36 -18
- package/skills/track-work/scripts/lib/document.mjs +17 -13
- package/skills/track-work/scripts/lib/format.mjs +2 -0
- package/skills/track-work/scripts/lib/recover.mjs +254 -0
- package/skills/track-work/scripts/lib/registry.mjs +656 -0
- package/skills/track-work/scripts/lib/search.mjs +40 -0
- package/skills/track-work/scripts/lib/store.mjs +46 -15
- package/skills/track-work/scripts/lib/views.mjs +50 -1
- package/skills/track-work/scripts/working-record.mjs +232 -13
- package/skills/verify-work/SKILL.md +45 -37
- package/skills/verify-work/references/feature-map.md +120 -0
- package/skills/verify-work/references/runtime-evidence.md +203 -0
- package/skills/verify-work/references/test-design.md +25 -6
- package/skills/verify-work/scripts/test-changes.mjs +246 -0
- package/skills/write-docs/SKILL.md +113 -25
- package/skills/write-docs/references/documentation-shapes.md +68 -3
- package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
- package/skills/write-docs/scripts/artifact-check.mjs +148 -0
- package/skills/write-docs/scripts/docs-check.mjs +96 -6
- package/skills/write-docs/scripts/plan-lint.mjs +123 -0
- package/skills/write-skill/SKILL.md +92 -52
- package/skills/write-skill/references/activation-and-wording.md +143 -0
- package/skills/write-skill/references/placement-and-dependencies.md +154 -0
- package/skills/write-skill/scripts/lib/compare.mjs +701 -0
- package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
- package/skills/write-skill/scripts/skill-author.mjs +51 -8
- package/capabilities/cognitive-routing/references/both-mode.md +0 -36
- package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
- package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
- package/plugin.json +0 -33
- package/skills/action-selection/SKILL.md +0 -92
- package/skills/action-selection/references/trajectory-stalls.md +0 -73
- package/skills/bypass/SKILL.md +0 -153
- package/skills/commit-work/SKILL.md +0 -93
- package/skills/commit-work/references/git-state-edges.md +0 -94
- package/skills/commit-work/references/simple-push.md +0 -94
- package/skills/commit-work/references/staging-decisions.md +0 -106
- package/skills/decision-gate/SKILL.md +0 -87
- package/skills/design-for-depth/SKILL.md +0 -108
- package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
- package/skills/design-for-depth/references/implementation-decisions.md +0 -76
- package/skills/design-for-depth/references/interface-design-loop.md +0 -105
- package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
- package/skills/diagnose-failure/SKILL.md +0 -140
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
- package/skills/discuss/SKILL.md +0 -140
- package/skills/evaluate-skill/SKILL.md +0 -188
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
- package/skills/evaluate-skill/references/evaluation-design.md +0 -112
- package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
- package/skills/evaluate-skill/references/review-and-revision.md +0 -94
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
- package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
- package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
- package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
- package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
- package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
- package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
- package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
- package/skills/execute-work/references/execute-work-edges.md +0 -78
- package/skills/finish-branch/SKILL.md +0 -117
- package/skills/finish-branch/references/integration-options.md +0 -80
- package/skills/handoff/SKILL.md +0 -82
- package/skills/handoff/references/handoff-templates.md +0 -150
- package/skills/launch-work/SKILL.md +0 -116
- package/skills/launch-work/references/launch-readiness.md +0 -85
- package/skills/migration-work/SKILL.md +0 -115
- package/skills/migration-work/references/migration-lifecycle.md +0 -82
- package/skills/release-work/SKILL.md +0 -118
- package/skills/release-work/references/release-evidence.md +0 -78
- package/skills/review-artifact/SKILL.md +0 -143
- package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
- package/skills/review-work/references/adjudicate-work-review.md +0 -97
- package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
- package/skills/review-work/references/security-risk-lens.md +0 -67
- package/skills/setup-freeflow/references/activation-contract.md +0 -103
- package/skills/setup-freeflow/references/host-setup.md +0 -67
- package/skills/simplify-code/SKILL.md +0 -85
- package/skills/simplify-code/references/simplification-patterns.md +0 -70
- package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
- package/skills/verify-work/references/integration-evidence.md +0 -49
- package/skills/verify-work/references/performance-evidence.md +0 -59
- package/skills/workflow/SKILL.md +0 -171
- package/skills/workflow/references/domain-skill-composition.md +0 -45
- package/skills/write-plan/SKILL.md +0 -92
- package/skills/write-plan/references/plan-shapes.md +0 -156
- package/skills/write-skill/references/activation-boundaries.md +0 -106
- package/skills/write-skill/references/agent-first-instructions.md +0 -133
- package/skills/write-skill/references/progressive-disclosure.md +0 -129
- package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
- package/skills/write-spec/SKILL.md +0 -85
- package/skills/write-spec/references/artifact-standards.md +0 -86
- package/skills/write-spec/references/decision-records.md +0 -80
- package/skills/write-spec/references/spec-shapes.md +0 -143
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// Container environments (optional): a configuration may run the subject's shell and the group's command graders
|
|
2
|
+
// in a digest-pinned image through the standard `docker` command line (Docker Desktop, Colima, OrbStack or Podman
|
|
3
|
+
// all provide it). Docker is looked for only when a group declares a container; an image is never pulled, and a
|
|
4
|
+
// missing CLI, daemon or image refuses the group with what to install or pull. A container environment is a shell
|
|
5
|
+
// isolation like any other: the shell tier's probes verify it before any subject runs.
|
|
6
|
+
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
|
|
13
|
+
const run = promisify(execFile);
|
|
14
|
+
const IMAGE = /^[^\s@]+@sha256:[0-9a-f]{64}$/;
|
|
15
|
+
const DOCKER_TIMEOUT_MS = 30_000;
|
|
16
|
+
/** Resource limits for every container the evaluator starts. */
|
|
17
|
+
export const CONTAINER_LIMITS = Object.freeze({ memory: "4g", cpus: "2", pids: "1024" });
|
|
18
|
+
const KEEP_ALIVE = "trap 'exit 0' TERM INT; while :; do sleep 3600 & wait $!; done";
|
|
19
|
+
// Run as root in a subject's container after it starts: replaces the declared workdir (empty, or absent, in the image)
|
|
20
|
+
// with a link to the workspace's mount at its host path. The image's own references to the workdir, such as
|
|
21
|
+
// GIT_WORK_TREE=/testbed or a script that changes into it, then reach the workspace under the same canonical path the
|
|
22
|
+
// shell starts in. A second mount would not do: Git compares the working directory with its work tree by path, so from
|
|
23
|
+
// a subfolder of one mount it would resolve paths against the other's root.
|
|
24
|
+
const LINK_WORKDIR = [
|
|
25
|
+
'if [ -d "$1" ] && [ ! -L "$1" ]; then',
|
|
26
|
+
' rmdir "$1" 2>/dev/null || { echo "the image has files at $1, where the workspace goes; use an image whose workdir is empty" >&2; exit 1; }',
|
|
27
|
+
"fi",
|
|
28
|
+
'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"',
|
|
29
|
+
].join("\n");
|
|
30
|
+
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validates a configuration's `environment`: `{ kind: "container", image: "<name>@sha256:<digest>", workdir,
|
|
37
|
+
* network: "none" }`.
|
|
38
|
+
*
|
|
39
|
+
* @param {any} value
|
|
40
|
+
* @param {string} label
|
|
41
|
+
* @param {(code: string, message: string) => never} fail
|
|
42
|
+
*/
|
|
43
|
+
export function validateContainerEnvironment(value, label, fail) {
|
|
44
|
+
if (!isPlainObject(value)) fail("invalid-definition", `${label} must be an object`);
|
|
45
|
+
for (const key of Object.keys(value)) {
|
|
46
|
+
if (!["kind", "image", "workdir", "network"].includes(key)) {
|
|
47
|
+
fail("invalid-definition", `${label} contains unsupported field ${key}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (value.kind !== "container") fail("invalid-definition", `${label}.kind must be container`);
|
|
51
|
+
if (typeof value.image !== "string" || !IMAGE.test(value.image)) {
|
|
52
|
+
fail(
|
|
53
|
+
"invalid-definition",
|
|
54
|
+
`${label}.image must be pinned by digest, such as name@sha256:<64 hex characters>; a tag can change under the run`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const workdir = value.workdir;
|
|
58
|
+
if (
|
|
59
|
+
typeof workdir !== "string" ||
|
|
60
|
+
!workdir.startsWith("/") ||
|
|
61
|
+
workdir === "/" ||
|
|
62
|
+
workdir.endsWith("/") ||
|
|
63
|
+
workdir
|
|
64
|
+
.split("/")
|
|
65
|
+
.slice(1)
|
|
66
|
+
.some((part) => part === "" || part === "." || part === "..")
|
|
67
|
+
) {
|
|
68
|
+
fail("invalid-definition", `${label}.workdir must be an absolute path such as /testbed`);
|
|
69
|
+
}
|
|
70
|
+
if (value.network !== "none") fail("invalid-definition", `${label}.network must be "none"`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The shell a container environment provides: bash runs in the container, which shows the workspace at its host
|
|
75
|
+
* path, where bash starts, and at workdir, a link to it; `workspace` is that guest name.
|
|
76
|
+
*/
|
|
77
|
+
export function containerShell(environment) {
|
|
78
|
+
if (environment?.kind !== "container") return undefined;
|
|
79
|
+
const container = { image: environment.image, workdir: environment.workdir, network: environment.network };
|
|
80
|
+
return { isolation: "container", network: environment.network, workspace: environment.workdir, container };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The docker CLI's endpoint, so a subject's isolated shell (which has no ~/.docker) reaches the same daemon. */
|
|
84
|
+
export async function dockerHost() {
|
|
85
|
+
if (process.env.DOCKER_HOST) return process.env.DOCKER_HOST;
|
|
86
|
+
try {
|
|
87
|
+
const { stdout } = await run("docker", ["context", "inspect", "--format", "{{.Endpoints.docker.Host}}"], {
|
|
88
|
+
timeout: DOCKER_TIMEOUT_MS,
|
|
89
|
+
});
|
|
90
|
+
return stdout.trim() || null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DOCKER_INSTALL = {
|
|
97
|
+
macos: "brew install colima docker && colima start (or install Docker Desktop or OrbStack)",
|
|
98
|
+
apt: "sudo apt-get install docker.io && sudo systemctl start docker (or install Docker Engine from docs.docker.com, or Podman with podman-docker)",
|
|
99
|
+
dnf: "sudo dnf install podman-docker (or Docker Engine from docs.docker.com, then sudo systemctl start docker)",
|
|
100
|
+
};
|
|
101
|
+
const DOCKER_START = {
|
|
102
|
+
macos: "colima start, or open Docker Desktop or OrbStack",
|
|
103
|
+
apt: "sudo systemctl start docker",
|
|
104
|
+
dnf: "sudo systemctl start docker (or podman machine start)",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* What is missing for a container environment, in the shell tier's refusal shape: the docker CLI, its daemon, the
|
|
109
|
+
* image present locally (with its pull command and, when the registry reports it, its download size), and, given the
|
|
110
|
+
* folder workspaces will live under, that the engine shares that folder with its containers.
|
|
111
|
+
*
|
|
112
|
+
* @param {{ image: string, workdir: string }} container
|
|
113
|
+
* @param {string | null} manager "macos", "apt", "dnf" or null
|
|
114
|
+
* @param {string | null} [workspaceRoot] the folder the run's workspaces are created under
|
|
115
|
+
*/
|
|
116
|
+
export async function missingContainerPrerequisites(container, manager, workspaceRoot = null) {
|
|
117
|
+
const reason = "the configuration runs the subject's shell and the command graders in a container";
|
|
118
|
+
try {
|
|
119
|
+
await run("docker", ["version", "--format", "{{.Server.Version}}"], { timeout: DOCKER_TIMEOUT_MS });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error?.code === "ENOENT") {
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
name: "a docker command line",
|
|
125
|
+
need: "docker on PATH (Docker Desktop, Colima, OrbStack or Podman's docker)",
|
|
126
|
+
found: null,
|
|
127
|
+
reason,
|
|
128
|
+
install: manager === null ? null : DOCKER_INSTALL[manager],
|
|
129
|
+
download: null,
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
name: "a running container engine",
|
|
136
|
+
need: "a docker daemon that answers `docker version`",
|
|
137
|
+
found: firstLine(error?.stderr) ?? "no daemon answered",
|
|
138
|
+
reason,
|
|
139
|
+
install: manager === null ? null : `start it: ${DOCKER_START[manager]}`,
|
|
140
|
+
download: null,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await run("docker", ["image", "inspect", container.image], { timeout: DOCKER_TIMEOUT_MS });
|
|
146
|
+
} catch {
|
|
147
|
+
const size = await imageDownloadSize(container.image);
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
name: `the image ${container.image}`,
|
|
151
|
+
need: "the image present locally (the evaluator never pulls one)",
|
|
152
|
+
found: null,
|
|
153
|
+
reason,
|
|
154
|
+
install: `docker pull ${container.image}`,
|
|
155
|
+
download: size === null ? null : `about ${formatBytes(size)} compressed, from docker manifest inspect`,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
const hidden = workspaceRoot === null ? null : await hiddenWorkspace(container, workspaceRoot);
|
|
160
|
+
if (hidden === null) return [];
|
|
161
|
+
return [
|
|
162
|
+
{
|
|
163
|
+
name: "a workspace folder the container engine can see",
|
|
164
|
+
need: `containers to see folders under ${workspaceRoot}`,
|
|
165
|
+
found: hidden,
|
|
166
|
+
reason,
|
|
167
|
+
install:
|
|
168
|
+
"a VM-based engine shares only some host folders (Colima, by default, only your home folder): run from a folder it shares (for check-tasks, set TMPDIR to one), pass --results there, or add the folder to the engine's mounts",
|
|
169
|
+
download: null,
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Whether a folder under `root` reaches containers. A VM-based engine (Colima, Docker Desktop, OrbStack, Podman's
|
|
176
|
+
* machine) mounts only the host folders it shares, and a workspace elsewhere appears empty inside the container, so
|
|
177
|
+
* graders and shells would run on nothing. Writes a marker in a fresh folder under `root` and reads it back through
|
|
178
|
+
* the image. Returns null when it arrives (or when the image has no `cat` to tell), else what went wrong.
|
|
179
|
+
*/
|
|
180
|
+
async function hiddenWorkspace(container, root) {
|
|
181
|
+
await mkdir(root, { recursive: true });
|
|
182
|
+
const probe = await mkdtemp(path.join(root, ".skill-eval-share-"));
|
|
183
|
+
const marker = randomBytes(8).toString("hex");
|
|
184
|
+
try {
|
|
185
|
+
await writeFile(path.join(probe, "marker"), marker);
|
|
186
|
+
const { stdout } = await run(
|
|
187
|
+
"docker",
|
|
188
|
+
[
|
|
189
|
+
"run",
|
|
190
|
+
"--rm",
|
|
191
|
+
"--label",
|
|
192
|
+
"skill-eval=1",
|
|
193
|
+
"--network",
|
|
194
|
+
"none",
|
|
195
|
+
"--volume",
|
|
196
|
+
`${probe}:${container.workdir}:ro`,
|
|
197
|
+
"--workdir",
|
|
198
|
+
container.workdir,
|
|
199
|
+
"--entrypoint",
|
|
200
|
+
"cat",
|
|
201
|
+
container.image,
|
|
202
|
+
"marker",
|
|
203
|
+
],
|
|
204
|
+
{ timeout: DOCKER_TIMEOUT_MS },
|
|
205
|
+
);
|
|
206
|
+
if (stdout.trim() === marker) return null;
|
|
207
|
+
return `the container read something other than the marker written in ${probe}`;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const detail = firstLine(error?.stderr) ?? String(error?.message ?? error);
|
|
210
|
+
// An image without `cat` cannot answer the question; it is not evidence of a hidden folder.
|
|
211
|
+
if (/executable file not found|no such file or directory: unknown|exec: "cat"/i.test(detail)) return null;
|
|
212
|
+
return `a marker written in ${probe} did not reach the container: ${detail}`;
|
|
213
|
+
} finally {
|
|
214
|
+
await rm(probe, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Compressed layer bytes for this machine's platform from `docker manifest inspect -v`, or null when unknown. */
|
|
219
|
+
async function imageDownloadSize(image) {
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
const { stdout } = await run("docker", ["manifest", "inspect", "-v", image], { timeout: DOCKER_TIMEOUT_MS });
|
|
223
|
+
parsed = JSON.parse(stdout);
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const entries = Array.isArray(parsed) ? parsed : [parsed];
|
|
228
|
+
const architecture = process.arch === "arm64" ? "arm64" : "amd64";
|
|
229
|
+
const entry =
|
|
230
|
+
entries.find(
|
|
231
|
+
(candidate) =>
|
|
232
|
+
candidate?.Descriptor?.platform?.os === "linux" && candidate.Descriptor.platform.architecture === architecture,
|
|
233
|
+
) ?? entries[0];
|
|
234
|
+
const layers = entry?.SchemaV2Manifest?.layers ?? entry?.OCIManifest?.layers;
|
|
235
|
+
if (!Array.isArray(layers) || layers.length === 0) return null;
|
|
236
|
+
return layers.reduce((total, layer) => total + (Number.isFinite(layer?.size) ? layer.size : 0), 0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function formatBytes(bytes) {
|
|
240
|
+
return bytes >= 1024 * 1024
|
|
241
|
+
? `${Math.round(bytes / (1024 * 1024))} MB`
|
|
242
|
+
: `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function firstLine(value) {
|
|
246
|
+
const line = String(value ?? "")
|
|
247
|
+
.split("\n")
|
|
248
|
+
.find((entry) => entry.trim() !== "");
|
|
249
|
+
return line?.trim() ?? null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// What the docker client needs from the user's environment to reach its engine, and nothing else.
|
|
253
|
+
const DOCKER_CLIENT_KEYS = [
|
|
254
|
+
"PATH",
|
|
255
|
+
"HOME",
|
|
256
|
+
"USER",
|
|
257
|
+
"LANG",
|
|
258
|
+
"TMPDIR",
|
|
259
|
+
"XDG_RUNTIME_DIR",
|
|
260
|
+
"DOCKER_HOST",
|
|
261
|
+
"DOCKER_CONTEXT",
|
|
262
|
+
"DOCKER_CONFIG",
|
|
263
|
+
"DOCKER_CERT_PATH",
|
|
264
|
+
"DOCKER_TLS_VERIFY",
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
/** The environment for a docker client the evaluator runs itself (a grader's `docker run`). */
|
|
268
|
+
export function dockerClientEnvironment() {
|
|
269
|
+
return Object.fromEntries(
|
|
270
|
+
DOCKER_CLIENT_KEYS.flatMap((key) => (process.env[key] === undefined ? [] : [[key, process.env[key]]])),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function containerName(purpose) {
|
|
275
|
+
return `skill-eval-${purpose}-${randomBytes(6).toString("hex")}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The options every evaluator container shares: limits, the declared network, the workspace mounted read-write at
|
|
280
|
+
* `at` and used as the working directory, and each declared resource (a skill, context, Freeflow's package) read-only
|
|
281
|
+
* at its own host path, so a path the subject was given, such as `node <skill>/scripts/x.mjs`, resolves in the
|
|
282
|
+
* container exactly as it does for `read`.
|
|
283
|
+
*/
|
|
284
|
+
function runOptions(container, { workspace, at, resources = [] }) {
|
|
285
|
+
const mounts = resources.flatMap((resource) => ["--volume", `${resource}:${resource}:ro`]);
|
|
286
|
+
return [
|
|
287
|
+
"--label",
|
|
288
|
+
"skill-eval=1",
|
|
289
|
+
"--network",
|
|
290
|
+
container.network,
|
|
291
|
+
"--memory",
|
|
292
|
+
CONTAINER_LIMITS.memory,
|
|
293
|
+
"--cpus",
|
|
294
|
+
CONTAINER_LIMITS.cpus,
|
|
295
|
+
"--pids-limit",
|
|
296
|
+
CONTAINER_LIMITS.pids,
|
|
297
|
+
"--volume",
|
|
298
|
+
`${workspace}:${at}`,
|
|
299
|
+
...mounts,
|
|
300
|
+
"--workdir",
|
|
301
|
+
at,
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Creates and starts a long-lived container for one subject (or one probe run). The workspace is mounted at its own
|
|
307
|
+
* canonical host path, which is also the shell's working directory, so `pwd`, the working directory the host tells
|
|
308
|
+
* the model and the paths its file tools take all name one directory; the declared workdir (such as /testbed) is a
|
|
309
|
+
* link to it. Each of `resources` (absolute host directories) is mounted read-only at the same path. The handle's
|
|
310
|
+
* `workdir` is where bash and declared commands run. `remove()` force-removes the container and is safe to call more
|
|
311
|
+
* than once; callers call it in `finally`, so a cancelled run removes it too.
|
|
312
|
+
*/
|
|
313
|
+
export async function startContainer({ container, workspace, resources = [], purpose = "subject" }) {
|
|
314
|
+
const name = containerName(purpose);
|
|
315
|
+
const host = await dockerHost();
|
|
316
|
+
// The host process (Pi, Claude Code) reports its working directory resolved, so the mount uses the same spelling.
|
|
317
|
+
const workdir = await realpath(workspace);
|
|
318
|
+
let removed = false;
|
|
319
|
+
const remove = async () => {
|
|
320
|
+
if (removed) return;
|
|
321
|
+
removed = true;
|
|
322
|
+
await run("docker", ["rm", "--force", name], { timeout: DOCKER_TIMEOUT_MS }).catch(() => {});
|
|
323
|
+
};
|
|
324
|
+
let step = "starting a container";
|
|
325
|
+
try {
|
|
326
|
+
await run(
|
|
327
|
+
"docker",
|
|
328
|
+
[
|
|
329
|
+
"create",
|
|
330
|
+
"--name",
|
|
331
|
+
name,
|
|
332
|
+
...runOptions(container, { workspace: workdir, at: workdir, resources }),
|
|
333
|
+
"--entrypoint",
|
|
334
|
+
"sh",
|
|
335
|
+
container.image,
|
|
336
|
+
"-c",
|
|
337
|
+
KEEP_ALIVE,
|
|
338
|
+
],
|
|
339
|
+
{ timeout: DOCKER_TIMEOUT_MS },
|
|
340
|
+
);
|
|
341
|
+
await run("docker", ["start", name], { timeout: DOCKER_TIMEOUT_MS });
|
|
342
|
+
step = `linking ${container.workdir} to the workspace in a container`;
|
|
343
|
+
await run("docker", ["exec", "--user", "0", name, "sh", "-c", LINK_WORKDIR, "sh", container.workdir, workdir], {
|
|
344
|
+
timeout: DOCKER_TIMEOUT_MS,
|
|
345
|
+
});
|
|
346
|
+
} catch (error) {
|
|
347
|
+
await remove();
|
|
348
|
+
throw new Error(`${step} from ${container.image} failed: ${firstLine(error?.stderr) ?? error.message}`);
|
|
349
|
+
}
|
|
350
|
+
return { name, host, workdir, remove };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The value the guard reads to route bash into a running container. */
|
|
354
|
+
export function containerTarget(handle) {
|
|
355
|
+
return JSON.stringify({ name: handle.name, host: handle.host, workdir: handle.workdir });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The running container from `containerTarget`'s value, or null when there is none. */
|
|
359
|
+
export function parseContainerTarget(value) {
|
|
360
|
+
if (!value) return null;
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON.parse(value);
|
|
363
|
+
return typeof parsed?.name === "string" && typeof parsed.workdir === "string" ? parsed : null;
|
|
364
|
+
} catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The docker client argv that runs a declared command's argv inside the running container, where bash runs. */
|
|
370
|
+
export function containerExecArgv(target, argv) {
|
|
371
|
+
const host = typeof target.host === "string" && target.host !== "" ? ["--host", target.host] : [];
|
|
372
|
+
return ["docker", ...host, "exec", "--workdir", target.workdir, target.name, ...argv];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The argv that runs a command grader in a fresh container on the graded copy, with the same limits and network.
|
|
377
|
+
* A grader is the author's, not the subject's, so it sees the copy at the declared workdir, where its argv expects
|
|
378
|
+
* it. The caller removes the container by name afterwards, so a timeout or cancellation leaves nothing running.
|
|
379
|
+
*/
|
|
380
|
+
export function graderArgv({ container, copy, argv, name }) {
|
|
381
|
+
return [
|
|
382
|
+
"docker",
|
|
383
|
+
"run",
|
|
384
|
+
"--rm",
|
|
385
|
+
"--name",
|
|
386
|
+
name,
|
|
387
|
+
...runOptions(container, { workspace: copy, at: container.workdir }),
|
|
388
|
+
container.image,
|
|
389
|
+
...argv,
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function removeContainer(name) {
|
|
394
|
+
await run("docker", ["rm", "--force", name], { timeout: DOCKER_TIMEOUT_MS }).catch(() => {});
|
|
395
|
+
}
|