@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
|
@@ -30,7 +30,14 @@ export async function prepareFixture({ declaredPath, root, groupDirectory }) {
|
|
|
30
30
|
const files = await fingerprintDirectory(sourcePath);
|
|
31
31
|
const snapshotPath = path.join(groupDirectory, "resources", "fixture");
|
|
32
32
|
await mkdir(path.dirname(snapshotPath), { recursive: true });
|
|
33
|
-
|
|
33
|
+
// Links are copied as they are: Node otherwise rewrites a relative link into an absolute one into the source.
|
|
34
|
+
await cp(sourcePath, snapshotPath, {
|
|
35
|
+
recursive: true,
|
|
36
|
+
dereference: false,
|
|
37
|
+
verbatimSymlinks: true,
|
|
38
|
+
errorOnExist: true,
|
|
39
|
+
force: false,
|
|
40
|
+
});
|
|
34
41
|
const snapshotFiles = await fingerprintDirectory(await realpath(snapshotPath));
|
|
35
42
|
const sourceFilesAfter = await fingerprintDirectory(sourcePath);
|
|
36
43
|
if (
|
|
@@ -57,11 +64,14 @@ export async function materializeFixture({ fixture, workspace }) {
|
|
|
57
64
|
await cp(fixture.snapshotPath, workspace, {
|
|
58
65
|
recursive: true,
|
|
59
66
|
dereference: false,
|
|
67
|
+
verbatimSymlinks: true,
|
|
60
68
|
errorOnExist: true,
|
|
61
69
|
force: false,
|
|
62
70
|
});
|
|
63
71
|
const materializedPath = await realpath(workspace);
|
|
64
|
-
|
|
72
|
+
// A workspace may hold links a subject made (an earlier stage's workspace), recorded rather than followed; a
|
|
73
|
+
// declared fixture's links were already checked when it was snapshotted.
|
|
74
|
+
const materializedFiles = await fingerprintDirectory(materializedPath, { links: "record" });
|
|
65
75
|
if (JSON.stringify(materializedFiles) !== JSON.stringify(fixture.files)) {
|
|
66
76
|
throw new Error(`declared fixture changed while it was being copied: ${fixture.declaredPath}`);
|
|
67
77
|
}
|
|
@@ -126,6 +136,7 @@ export async function materializeRuntime({ runtime, variantDirectory }) {
|
|
|
126
136
|
await cp(bundle.root, destination, {
|
|
127
137
|
recursive: true,
|
|
128
138
|
dereference: false,
|
|
139
|
+
verbatimSymlinks: true,
|
|
129
140
|
errorOnExist: true,
|
|
130
141
|
force: false,
|
|
131
142
|
});
|
|
@@ -228,13 +239,84 @@ export async function materializeEnvironment({ environment, root, variantDirecto
|
|
|
228
239
|
};
|
|
229
240
|
}
|
|
230
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Snapshots each of the group's two arms' target skill into `destinationRoot/<arm>` and checks that the two differ
|
|
244
|
+
* only in the frontmatter description: every other file, and SKILL.md outside its description, must be
|
|
245
|
+
* byte-identical. Throws with the first difference found.
|
|
246
|
+
*/
|
|
247
|
+
export async function verifyDescriptionOnlyRevision({ variants, root, destinationRoot }) {
|
|
248
|
+
const canonicalRoot = await realpath(root);
|
|
249
|
+
const snapshots = {};
|
|
250
|
+
const [reference, revised] = Object.keys(variants);
|
|
251
|
+
for (const variant of [reference, revised]) {
|
|
252
|
+
const environment = variants[variant];
|
|
253
|
+
const declaredPath = environment.skills[environment.target];
|
|
254
|
+
const destination = path.join(destinationRoot, variant);
|
|
255
|
+
if (environment.source.kind === "working-tree") {
|
|
256
|
+
snapshots[variant] = await snapshotWorkingTreeResource({
|
|
257
|
+
canonicalRoot,
|
|
258
|
+
declaredPath,
|
|
259
|
+
destination,
|
|
260
|
+
requireDirectory: true,
|
|
261
|
+
label: "skill",
|
|
262
|
+
});
|
|
263
|
+
} else {
|
|
264
|
+
const git = await prepareGitSource(canonicalRoot, environment.source.ref);
|
|
265
|
+
snapshots[variant] = await snapshotGitResource({
|
|
266
|
+
...git,
|
|
267
|
+
declaredPath,
|
|
268
|
+
destination,
|
|
269
|
+
requireDirectory: true,
|
|
270
|
+
label: "skill",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const baselineFiles = new Map(snapshots[reference].files.map((file) => [file.path, file]));
|
|
275
|
+
const candidateFiles = new Map(snapshots[revised].files.map((file) => [file.path, file]));
|
|
276
|
+
for (const filePath of new Set([...baselineFiles.keys(), ...candidateFiles.keys()])) {
|
|
277
|
+
if (filePath === "SKILL.md") continue;
|
|
278
|
+
const baselineFile = baselineFiles.get(filePath);
|
|
279
|
+
const candidateFile = candidateFiles.get(filePath);
|
|
280
|
+
if (!baselineFile || !candidateFile || baselineFile.sha256 !== candidateFile.sha256) {
|
|
281
|
+
throw new Error(`description-only revision changes ${filePath} outside SKILL.md`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const [baselineSkill, candidateSkill] = await Promise.all(
|
|
285
|
+
[snapshots[reference], snapshots[revised]].map((snapshot) =>
|
|
286
|
+
readFile(path.join(snapshot.path, "SKILL.md"), "utf8"),
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
if (withoutDescription(baselineSkill) !== withoutDescription(candidateSkill)) {
|
|
290
|
+
throw new Error("description-only revision changes SKILL.md outside its frontmatter description");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Removes the frontmatter `description` entry (one line, or a block scalar with its indented continuation).
|
|
295
|
+
function withoutDescription(text) {
|
|
296
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/.exec(text);
|
|
297
|
+
if (!match) return text;
|
|
298
|
+
const kept = [];
|
|
299
|
+
let skipping = false;
|
|
300
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
301
|
+
if (/^description\s*:/.test(line)) {
|
|
302
|
+
skipping = true;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (skipping && /^\s+\S/.test(line)) continue;
|
|
306
|
+
skipping = false;
|
|
307
|
+
kept.push(line);
|
|
308
|
+
}
|
|
309
|
+
return `---\n${kept.join("\n")}\n---${match[2]}${text.slice(match[0].length)}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
231
312
|
export async function verifyEnvironment(environment) {
|
|
232
313
|
for (const [label, resources] of [
|
|
233
314
|
["skill", environment.skills],
|
|
234
315
|
["context", environment.context],
|
|
235
316
|
]) {
|
|
236
317
|
for (const resource of resources) {
|
|
237
|
-
|
|
318
|
+
// An earlier stage's delivered work may hold links its subject made; it is checked the way it was recorded.
|
|
319
|
+
const current = await fingerprintDirectory(resource.path, { links: resource.links ?? "strict" });
|
|
238
320
|
if (JSON.stringify(current) !== JSON.stringify(resource.files)) {
|
|
239
321
|
throw new Error(`declared ${label} changed during subject execution: ${resource.declaredPath}`);
|
|
240
322
|
}
|
|
@@ -253,9 +335,12 @@ export async function snapshotWorkspace({ workspace, variantDirectory, turn, bef
|
|
|
253
335
|
await rm(snapshotPath, { recursive: true, force: true });
|
|
254
336
|
await mkdir(path.dirname(snapshotPath), { recursive: true });
|
|
255
337
|
|
|
256
|
-
const sourceBefore = await fingerprintDirectory(workspace);
|
|
338
|
+
const sourceBefore = await fingerprintDirectory(workspace, WORKSPACE);
|
|
257
339
|
await cp(workspace, snapshotPath, { recursive: true, force: false, errorOnExist: true, verbatimSymlinks: true });
|
|
258
|
-
const [sourceAfter, files] = await Promise.all([
|
|
340
|
+
const [sourceAfter, files] = await Promise.all([
|
|
341
|
+
fingerprintDirectory(workspace, WORKSPACE),
|
|
342
|
+
fingerprintDirectory(snapshotPath, WORKSPACE),
|
|
343
|
+
]);
|
|
259
344
|
if (
|
|
260
345
|
JSON.stringify(sourceBefore) !== JSON.stringify(sourceAfter) ||
|
|
261
346
|
JSON.stringify(sourceAfter) !== JSON.stringify(files)
|
|
@@ -265,10 +350,22 @@ export async function snapshotWorkspace({ workspace, variantDirectory, turn, bef
|
|
|
265
350
|
return { path: snapshotPath, files, changes: workspaceChanges(before, files) };
|
|
266
351
|
}
|
|
267
352
|
|
|
268
|
-
|
|
353
|
+
/** How a subject's workspace is fingerprinted: its links are the subject's own work, recorded and never followed. */
|
|
354
|
+
export const WORKSPACE = Object.freeze({ links: "record" });
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Every entry under `root`, sorted: files and links with a SHA-256, directories, and each link's target. A declared
|
|
358
|
+
* resource (`links: "strict"`, the default) may hold only relative links that stay inside it. A subject's workspace
|
|
359
|
+
* (`links: "record"`) keeps any link the subject made, such as one an installer pointed at the container's
|
|
360
|
+
* workdir: it is recorded with its target, as a link, and nothing outside is ever read through it.
|
|
361
|
+
*
|
|
362
|
+
* @param {string} root
|
|
363
|
+
* @param {{ links?: "strict" | "record" }} [options]
|
|
364
|
+
*/
|
|
365
|
+
export async function fingerprintDirectory(root, { links = "strict" } = {}) {
|
|
269
366
|
const canonicalRoot = await realpath(root);
|
|
270
367
|
const files = [];
|
|
271
|
-
await walk(canonicalRoot, canonicalRoot, files);
|
|
368
|
+
await walk(canonicalRoot, canonicalRoot, files, links);
|
|
272
369
|
return files.sort((left, right) => comparePaths(left.path, right.path));
|
|
273
370
|
}
|
|
274
371
|
|
|
@@ -304,7 +401,13 @@ async function snapshotWorkingTreeResource({ canonicalRoot, declaredPath, destin
|
|
|
304
401
|
: [{ path: path.basename(sourcePath), type: "file", sha256: sha256(await readFile(sourcePath)) }];
|
|
305
402
|
if (sourceStat.isDirectory()) {
|
|
306
403
|
await mkdir(path.dirname(destination), { recursive: true });
|
|
307
|
-
await cp(sourcePath, destination, {
|
|
404
|
+
await cp(sourcePath, destination, {
|
|
405
|
+
recursive: true,
|
|
406
|
+
dereference: false,
|
|
407
|
+
verbatimSymlinks: true,
|
|
408
|
+
errorOnExist: true,
|
|
409
|
+
force: false,
|
|
410
|
+
});
|
|
308
411
|
} else {
|
|
309
412
|
await mkdir(destination, { recursive: true });
|
|
310
413
|
await copyFile(sourcePath, path.join(destination, path.basename(sourcePath)));
|
|
@@ -462,7 +565,7 @@ async function contextFiles(root, fingerprints) {
|
|
|
462
565
|
return files;
|
|
463
566
|
}
|
|
464
567
|
|
|
465
|
-
async function walk(root, directory, files) {
|
|
568
|
+
async function walk(root, directory, files, links) {
|
|
466
569
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
467
570
|
entries.sort((left, right) => comparePaths(left.name, right.name));
|
|
468
571
|
for (const entry of entries) {
|
|
@@ -471,14 +574,16 @@ async function walk(root, directory, files) {
|
|
|
471
574
|
const entryStat = await lstat(entryPath);
|
|
472
575
|
if (entryStat.isSymbolicLink()) {
|
|
473
576
|
const target = await readlink(entryPath);
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
577
|
+
if (links !== "record") {
|
|
578
|
+
const canonicalTarget = await realpath(entryPath).catch(() => null);
|
|
579
|
+
if (path.isAbsolute(target) || canonicalTarget === null || !isContained(root, canonicalTarget)) {
|
|
580
|
+
throw new Error(`resource symlink escapes its declared root: ${relativePath}`);
|
|
581
|
+
}
|
|
477
582
|
}
|
|
478
|
-
files.push({ path: relativePath, type: "symlink", sha256: sha256(`symlink\0${target}`) });
|
|
583
|
+
files.push({ path: relativePath, type: "symlink", sha256: sha256(`symlink\0${target}`), target });
|
|
479
584
|
} else if (entryStat.isDirectory()) {
|
|
480
585
|
files.push({ path: relativePath, type: "directory", sha256: sha256("directory") });
|
|
481
|
-
await walk(root, entryPath, files);
|
|
586
|
+
await walk(root, entryPath, files, links);
|
|
482
587
|
} else if (entryStat.isFile()) {
|
|
483
588
|
files.push({ path: relativePath, type: "file", sha256: sha256(await readFile(entryPath)) });
|
|
484
589
|
} else {
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
// The transcript scan's reading of a bash command: its simple commands, each as its words, and the network targets
|
|
2
|
+
// they reach for. It is a heuristic over shell text, not a shell: it follows quotes, `bash -c` strings, command
|
|
3
|
+
// substitutions and heredocs fed to a shell, and expands variables assigned a literal earlier in the same command.
|
|
4
|
+
// A target it cannot resolve (a variable read from a file) is not shown to be external, so it is not reported.
|
|
5
|
+
|
|
6
|
+
const SHELLS = new Set(["bash", "sh", "zsh", "dash", "ksh"]);
|
|
7
|
+
// Programs that only run the program after them.
|
|
8
|
+
const WRAPPERS = new Set(["sudo", "nohup", "command", "exec", "time", "nice", "stdbuf", "caffeinate"]);
|
|
9
|
+
const MAX_DEPTH = 4;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The simple commands of a shell command line, each `{ words, bodies }`: its words with quotes removed, and the
|
|
13
|
+
* text of each heredoc fed to it. A string run by a shell (`bash -c`), a command substitution and a heredoc fed to a
|
|
14
|
+
* shell are parsed as commands too; a heredoc fed to anything else (a script for `node`, a JSON payload) is data.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} text
|
|
17
|
+
* @returns {{ words: string[], bodies: string[] }[]}
|
|
18
|
+
*/
|
|
19
|
+
export function simpleCommands(text, depth = 0) {
|
|
20
|
+
const commands = [];
|
|
21
|
+
const nested = [];
|
|
22
|
+
let current = { words: [], bodies: [] };
|
|
23
|
+
let word = null;
|
|
24
|
+
let heredocs = [];
|
|
25
|
+
const endWord = () => {
|
|
26
|
+
if (word !== null) current.words.push(word);
|
|
27
|
+
word = null;
|
|
28
|
+
};
|
|
29
|
+
const endCommand = () => {
|
|
30
|
+
endWord();
|
|
31
|
+
if (current.words.length > 0) commands.push(current);
|
|
32
|
+
current = { words: [], bodies: [] };
|
|
33
|
+
};
|
|
34
|
+
let index = 0;
|
|
35
|
+
while (index < text.length) {
|
|
36
|
+
const char = text[index];
|
|
37
|
+
if (char === "\\") {
|
|
38
|
+
if (text[index + 1] !== "\n") word = (word ?? "") + (text[index + 1] ?? "");
|
|
39
|
+
index += 2;
|
|
40
|
+
} else if (char === "'") {
|
|
41
|
+
const end = text.indexOf("'", index + 1);
|
|
42
|
+
const stop = end === -1 ? text.length : end;
|
|
43
|
+
word = (word ?? "") + text.slice(index + 1, stop);
|
|
44
|
+
index = stop + 1;
|
|
45
|
+
} else if (char === '"') {
|
|
46
|
+
let cursor = index + 1;
|
|
47
|
+
let value = "";
|
|
48
|
+
while (cursor < text.length && text[cursor] !== '"') {
|
|
49
|
+
if (text[cursor] === "\\" && cursor + 1 < text.length) {
|
|
50
|
+
value += text[cursor + 1];
|
|
51
|
+
cursor += 2;
|
|
52
|
+
} else {
|
|
53
|
+
value += text[cursor];
|
|
54
|
+
cursor += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
word = (word ?? "") + value;
|
|
58
|
+
index = cursor + 1;
|
|
59
|
+
} else if (char === "$" && text[index + 1] === "(" && text[index + 2] !== "(") {
|
|
60
|
+
const end = closing(text, index + 1);
|
|
61
|
+
nested.push(text.slice(index + 2, end));
|
|
62
|
+
word = (word ?? "") + text.slice(index, end + 1);
|
|
63
|
+
index = end + 1;
|
|
64
|
+
} else if (char === "`") {
|
|
65
|
+
const end = text.indexOf("`", index + 1);
|
|
66
|
+
const stop = end === -1 ? text.length : end;
|
|
67
|
+
nested.push(text.slice(index + 1, stop));
|
|
68
|
+
word = (word ?? "") + text.slice(index, stop + 1);
|
|
69
|
+
index = stop + 1;
|
|
70
|
+
} else if (char === "#" && word === null) {
|
|
71
|
+
const end = text.indexOf("\n", index);
|
|
72
|
+
index = end === -1 ? text.length : end;
|
|
73
|
+
} else if (char === "<" && text[index + 1] === "<" && text[index + 2] !== "<") {
|
|
74
|
+
endWord();
|
|
75
|
+
let cursor = index + 2;
|
|
76
|
+
const strip = text[cursor] === "-";
|
|
77
|
+
if (strip) cursor += 1;
|
|
78
|
+
while (text[cursor] === " " || text[cursor] === "\t") cursor += 1;
|
|
79
|
+
let delimiter = "";
|
|
80
|
+
while (cursor < text.length && !/[\s;&|<>()]/.test(text[cursor])) {
|
|
81
|
+
if (text[cursor] !== "'" && text[cursor] !== '"' && text[cursor] !== "\\") delimiter += text[cursor];
|
|
82
|
+
cursor += 1;
|
|
83
|
+
}
|
|
84
|
+
heredocs.push({ delimiter, strip, owner: current });
|
|
85
|
+
index = cursor;
|
|
86
|
+
} else if (char === "<" || char === ">") {
|
|
87
|
+
endWord();
|
|
88
|
+
index += 1;
|
|
89
|
+
// `2>&1`, `>&2`: a descriptor, not a command separator.
|
|
90
|
+
if (text[index] === "&") index += 1;
|
|
91
|
+
while (text[index] === ">" || text[index] === "|") index += 1;
|
|
92
|
+
} else if (char === "\n") {
|
|
93
|
+
endCommand();
|
|
94
|
+
index += 1;
|
|
95
|
+
for (const heredoc of heredocs) {
|
|
96
|
+
const body = [];
|
|
97
|
+
while (index < text.length) {
|
|
98
|
+
const end = text.indexOf("\n", index);
|
|
99
|
+
const line = text.slice(index, end === -1 ? text.length : end);
|
|
100
|
+
index = end === -1 ? text.length : end + 1;
|
|
101
|
+
if ((heredoc.strip ? line.replace(/^\t+/, "") : line) === heredoc.delimiter) break;
|
|
102
|
+
body.push(line);
|
|
103
|
+
}
|
|
104
|
+
heredoc.owner.bodies.push(body.join("\n"));
|
|
105
|
+
}
|
|
106
|
+
heredocs = [];
|
|
107
|
+
} else if (";&|()".includes(char)) {
|
|
108
|
+
endCommand();
|
|
109
|
+
index += 1;
|
|
110
|
+
} else if (char === " " || char === "\t" || char === "\r") {
|
|
111
|
+
endWord();
|
|
112
|
+
index += 1;
|
|
113
|
+
} else {
|
|
114
|
+
word = (word ?? "") + char;
|
|
115
|
+
index += 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
endCommand();
|
|
119
|
+
if (depth >= MAX_DEPTH) return commands;
|
|
120
|
+
const all = [];
|
|
121
|
+
for (const command of commands) {
|
|
122
|
+
all.push(command);
|
|
123
|
+
const { name, args } = program(command.words);
|
|
124
|
+
if (SHELLS.has(name)) {
|
|
125
|
+
const flag = args.findIndex((arg) => /^-[a-z]*c[a-z]*$/.test(arg));
|
|
126
|
+
if (flag !== -1 && typeof args[flag + 1] === "string") all.push(...simpleCommands(args[flag + 1], depth + 1));
|
|
127
|
+
for (const body of command.bodies) all.push(...simpleCommands(body, depth + 1));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const inner of nested) all.push(...simpleCommands(inner, depth + 1));
|
|
131
|
+
return all;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The index of the parenthesis that closes the one at `open`, or the text's end. */
|
|
135
|
+
function closing(text, open) {
|
|
136
|
+
let depth = 0;
|
|
137
|
+
for (let index = open; index < text.length; index += 1) {
|
|
138
|
+
if (text[index] === "(") depth += 1;
|
|
139
|
+
else if (text[index] === ")") {
|
|
140
|
+
depth -= 1;
|
|
141
|
+
if (depth === 0) return index;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return text.length;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s;
|
|
148
|
+
|
|
149
|
+
/** The program a simple command runs and its arguments: leading assignments and wrappers (`sudo`, `env`) skipped. */
|
|
150
|
+
export function program(words) {
|
|
151
|
+
let index = 0;
|
|
152
|
+
while (index < words.length) {
|
|
153
|
+
const word = words[index];
|
|
154
|
+
if (ASSIGNMENT.test(word)) {
|
|
155
|
+
index += 1;
|
|
156
|
+
} else if (WRAPPERS.has(word)) {
|
|
157
|
+
index += 1;
|
|
158
|
+
while (index < words.length && words[index].startsWith("-")) index += 1;
|
|
159
|
+
} else if (word === "env") {
|
|
160
|
+
index += 1;
|
|
161
|
+
while (index < words.length && (words[index].startsWith("-") || ASSIGNMENT.test(words[index]))) index += 1;
|
|
162
|
+
} else if (word === "timeout") {
|
|
163
|
+
index += 1;
|
|
164
|
+
while (index < words.length && words[index].startsWith("-")) index += 1;
|
|
165
|
+
index += 1;
|
|
166
|
+
} else {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const name = (words[index] ?? "").split("/").at(-1);
|
|
171
|
+
return { name, args: words.slice(index + 1) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Variables a command line assigns a literal, in order: `NAME=value` and `export NAME=value`. */
|
|
175
|
+
export function assignments(commands) {
|
|
176
|
+
const values = new Map();
|
|
177
|
+
for (const { words } of commands) {
|
|
178
|
+
for (const word of words[0] === "export" || words[0] === "local" ? words.slice(1) : words) {
|
|
179
|
+
const match = ASSIGNMENT.exec(word);
|
|
180
|
+
if (match === null) break;
|
|
181
|
+
if (!match[2].includes("$(") && !match[2].includes("`")) values.set(match[1], expand(match[2], values));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return values;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** `$NAME` and `${NAME}` replaced by their literal values; unknown variables are left as they are. */
|
|
188
|
+
export function expand(word, values) {
|
|
189
|
+
return word.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (whole, braced, bare) => {
|
|
190
|
+
const value = values.get(braced ?? bare);
|
|
191
|
+
return value === undefined ? whole : value;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Whether a host is this machine or the container's own network: loopback, `0.0.0.0`, `*.localhost`, or a name with
|
|
197
|
+
* no dot (a container's service name, which a sealed run cannot resolve beyond its own network anyway).
|
|
198
|
+
*/
|
|
199
|
+
export function isInternalHost(host) {
|
|
200
|
+
const name = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
201
|
+
if (name === "localhost" || name.endsWith(".localhost") || name === "0.0.0.0" || name === "::1") return true;
|
|
202
|
+
if (/^127(?:\.\d{1,3}){3}$/.test(name)) return true;
|
|
203
|
+
return !name.includes(".") && !name.includes(":");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const SCHEME_URL = /^([a-z][a-z0-9+.-]*):\/\/(?:[^@/?#\s]*@)?(\[[^\]]+\]|[^:/?#\s]+)/i;
|
|
207
|
+
const BARE_HOST = /^(?:[^@/\s]+@)?([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+)(?::\d+)?(?:[/?#].*)?$/s;
|
|
208
|
+
|
|
209
|
+
// A bare word ending like a file name is a file, not a host (`out.json`, `notes.md`).
|
|
210
|
+
const FILE_ENDING = /\.(?:json|md|txt|log|js|mjs|cjs|ts|html|css|csv|xml|ya?ml|sh|py|out|body|tmp|lock|tgz|gz|zip)$/i;
|
|
211
|
+
|
|
212
|
+
/** The host a URL (or, with `bare`, a `host[:port]/path`) names; null when it is neither or still has a variable. */
|
|
213
|
+
function hostOf(word, { bare = true } = {}) {
|
|
214
|
+
if (word.includes("$")) return null;
|
|
215
|
+
const url = SCHEME_URL.exec(word);
|
|
216
|
+
if (url !== null) return url[1].toLowerCase() === "file" ? null : url[2];
|
|
217
|
+
if (!bare) return null;
|
|
218
|
+
const host = BARE_HOST.exec(word)?.[1] ?? null;
|
|
219
|
+
if (host === null || FILE_ENDING.test(host)) return null;
|
|
220
|
+
// A version or other number (1.2.3) is no host; a dotted IPv4 address is.
|
|
221
|
+
if (/^\d+(?:\.\d+)*$/.test(host) && !/^\d+\.\d+\.\d+\.\d+$/.test(host)) return null;
|
|
222
|
+
return host;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Options of an HTTP client that take a value: the value is a header, payload, file or number, never the target.
|
|
226
|
+
const HTTP_VALUE_OPTIONS = new Set([
|
|
227
|
+
"-A",
|
|
228
|
+
"-b",
|
|
229
|
+
"-c",
|
|
230
|
+
"-C",
|
|
231
|
+
"-d",
|
|
232
|
+
"-D",
|
|
233
|
+
"-e",
|
|
234
|
+
"-E",
|
|
235
|
+
"-F",
|
|
236
|
+
"-H",
|
|
237
|
+
"-K",
|
|
238
|
+
"-m",
|
|
239
|
+
"-o",
|
|
240
|
+
"-O",
|
|
241
|
+
"-P",
|
|
242
|
+
"-r",
|
|
243
|
+
"-T",
|
|
244
|
+
"-t",
|
|
245
|
+
"-u",
|
|
246
|
+
"-U",
|
|
247
|
+
"-w",
|
|
248
|
+
"-X",
|
|
249
|
+
"-a",
|
|
250
|
+
"-i",
|
|
251
|
+
"--cacert",
|
|
252
|
+
"--cert",
|
|
253
|
+
"--config",
|
|
254
|
+
"--connect-timeout",
|
|
255
|
+
"--cookie",
|
|
256
|
+
"--cookie-jar",
|
|
257
|
+
"--data",
|
|
258
|
+
"--data-ascii",
|
|
259
|
+
"--data-binary",
|
|
260
|
+
"--data-raw",
|
|
261
|
+
"--data-urlencode",
|
|
262
|
+
"--dump-header",
|
|
263
|
+
"--form",
|
|
264
|
+
"--header",
|
|
265
|
+
"--key",
|
|
266
|
+
"--max-time",
|
|
267
|
+
"--output",
|
|
268
|
+
"--output-document",
|
|
269
|
+
"--post-data",
|
|
270
|
+
"--post-file",
|
|
271
|
+
"--referer",
|
|
272
|
+
"--request",
|
|
273
|
+
"--retry",
|
|
274
|
+
"--timeout",
|
|
275
|
+
"--tries",
|
|
276
|
+
"--upload-file",
|
|
277
|
+
"--user",
|
|
278
|
+
"--user-agent",
|
|
279
|
+
"--write-out",
|
|
280
|
+
]);
|
|
281
|
+
const SOCKET_VALUE_OPTIONS = new Set(["-w", "-p", "-s", "-x", "-X", "-i", "-q", "-O", "-I", "-T", "-V"]);
|
|
282
|
+
const SSH_VALUE_OPTIONS = new Set([
|
|
283
|
+
"-p",
|
|
284
|
+
"-P",
|
|
285
|
+
"-i",
|
|
286
|
+
"-l",
|
|
287
|
+
"-o",
|
|
288
|
+
"-F",
|
|
289
|
+
"-J",
|
|
290
|
+
"-b",
|
|
291
|
+
"-c",
|
|
292
|
+
"-D",
|
|
293
|
+
"-E",
|
|
294
|
+
"-e",
|
|
295
|
+
"-L",
|
|
296
|
+
"-m",
|
|
297
|
+
"-O",
|
|
298
|
+
"-Q",
|
|
299
|
+
"-R",
|
|
300
|
+
"-S",
|
|
301
|
+
"-W",
|
|
302
|
+
"-w",
|
|
303
|
+
"-B",
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
/** Positional arguments, with the values of the given options skipped. */
|
|
307
|
+
function positionals(args, valueOptions) {
|
|
308
|
+
const found = [];
|
|
309
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
310
|
+
const arg = args[index];
|
|
311
|
+
if (arg === "--url") {
|
|
312
|
+
if (args[index + 1] !== undefined) found.push(args[index + 1]);
|
|
313
|
+
index += 1;
|
|
314
|
+
} else if (valueOptions.has(arg)) {
|
|
315
|
+
index += 1;
|
|
316
|
+
} else if (!arg.startsWith("-") || arg === "-") {
|
|
317
|
+
if (arg !== "-") found.push(arg);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return found;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const PACKAGE_MANAGERS = {
|
|
324
|
+
npm: ["install", "i", "add", "update", "up", "upgrade"],
|
|
325
|
+
pnpm: ["install", "i", "add", "update", "up"],
|
|
326
|
+
yarn: ["add", "upgrade", "up"],
|
|
327
|
+
bun: ["add", "install", "i"],
|
|
328
|
+
pip: ["install", "download"],
|
|
329
|
+
pip3: ["install", "download"],
|
|
330
|
+
gem: ["install"],
|
|
331
|
+
cargo: ["install", "add", "fetch"],
|
|
332
|
+
go: ["get", "install"],
|
|
333
|
+
};
|
|
334
|
+
const SYSTEM_PACKAGES = {
|
|
335
|
+
apt: ["install", "update", "upgrade", "dist-upgrade"],
|
|
336
|
+
"apt-get": ["install", "update", "upgrade", "dist-upgrade"],
|
|
337
|
+
apk: ["add", "update", "upgrade"],
|
|
338
|
+
brew: ["install", "update", "upgrade", "tap"],
|
|
339
|
+
yum: ["install", "update", "upgrade"],
|
|
340
|
+
dnf: ["install", "update", "upgrade"],
|
|
341
|
+
};
|
|
342
|
+
// Options of a package manager that take a value that is not a package.
|
|
343
|
+
const PACKAGE_VALUE_OPTIONS = new Set([
|
|
344
|
+
"--prefix",
|
|
345
|
+
"-C",
|
|
346
|
+
"--cache",
|
|
347
|
+
"--registry",
|
|
348
|
+
"--workspace",
|
|
349
|
+
"-w",
|
|
350
|
+
"--dir",
|
|
351
|
+
"--cwd",
|
|
352
|
+
"-t",
|
|
353
|
+
"--target",
|
|
354
|
+
"-r",
|
|
355
|
+
"--requirement",
|
|
356
|
+
"-c",
|
|
357
|
+
"--constraint",
|
|
358
|
+
"-i",
|
|
359
|
+
"--index-url",
|
|
360
|
+
"--extra-index-url",
|
|
361
|
+
"--root",
|
|
362
|
+
]);
|
|
363
|
+
|
|
364
|
+
/** Whether a package spec names something local (a path, a file: or link: spec, a tarball beside it). */
|
|
365
|
+
function localSpec(spec) {
|
|
366
|
+
return /^(?:\.|\/|~|file:|link:|workspace:|portal:)/.test(spec) || /\.(?:tgz|tar\.gz|whl|gem)$/.test(spec);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The network attempts in one bash command: each `{ program, target }`. An HTTP client, socket tool, remote shell or
|
|
371
|
+
* Git remote counts only with an external target; a package install counts only when `sealed` (the run's network
|
|
372
|
+
* was "none") and it names a package from a registry, since installing the project's own dependencies or a local
|
|
373
|
+
* path is ordinary work. Code in the command (a `node -e` script, a heredoc) counts when it fetches an external URL.
|
|
374
|
+
*
|
|
375
|
+
* @param {string} command
|
|
376
|
+
* @param {{ sealed?: boolean }} [options]
|
|
377
|
+
*/
|
|
378
|
+
export function networkAttempts(command, { sealed = false } = {}) {
|
|
379
|
+
const commands = simpleCommands(command);
|
|
380
|
+
const values = assignments(commands);
|
|
381
|
+
const attempts = [];
|
|
382
|
+
const external = (name, words, options) => {
|
|
383
|
+
for (const word of words) {
|
|
384
|
+
const host = hostOf(expand(word, values), options);
|
|
385
|
+
if (host !== null && !isInternalHost(host)) attempts.push({ program: name, target: expand(word, values) });
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
for (const { words } of commands) {
|
|
389
|
+
const { name, args } = program(words);
|
|
390
|
+
if (["curl", "wget", "http", "https", "xh"].includes(name)) {
|
|
391
|
+
external(name, positionals(args, HTTP_VALUE_OPTIONS));
|
|
392
|
+
} else if (["nc", "ncat", "netcat", "telnet"].includes(name)) {
|
|
393
|
+
external(name, positionals(args, SOCKET_VALUE_OPTIONS).slice(0, 1), { bare: true });
|
|
394
|
+
} else if (name === "ssh") {
|
|
395
|
+
// A remote shell's host may be an alias with no dot, so only a loopback name is this machine.
|
|
396
|
+
const [host] = positionals(args, SSH_VALUE_OPTIONS);
|
|
397
|
+
const target = host === undefined ? null : expand(host, values).replace(/^[^@]*@/, "");
|
|
398
|
+
if (target !== null && !target.includes("$") && !isLoopbackName(target)) attempts.push({ program: name, target });
|
|
399
|
+
} else if (["scp", "sftp", "rsync"].includes(name)) {
|
|
400
|
+
for (const arg of positionals(args, SSH_VALUE_OPTIONS)) {
|
|
401
|
+
const value = expand(arg, values);
|
|
402
|
+
const remote = /^(?:[^@/\s]+@)?([^:/\s]+):/.exec(value);
|
|
403
|
+
const host =
|
|
404
|
+
SCHEME_URL.exec(value)?.[2] ?? (value.startsWith("/") || value.startsWith(".") ? null : remote?.[1]);
|
|
405
|
+
if (host && !host.includes("$") && !isLoopbackName(host)) attempts.push({ program: name, target: value });
|
|
406
|
+
}
|
|
407
|
+
} else if (name === "git") {
|
|
408
|
+
const rest = args.filter((arg, index) => !(args[index - 1] === "-C" || args[index - 1] === "-c"));
|
|
409
|
+
const subcommand = rest.find((arg) => !arg.startsWith("-") && !ASSIGNMENT.test(arg));
|
|
410
|
+
if (["clone", "fetch", "pull", "push", "ls-remote"].includes(subcommand)) {
|
|
411
|
+
const after = rest.slice(rest.indexOf(subcommand) + 1).filter((arg) => !arg.startsWith("-"));
|
|
412
|
+
const remotes = after
|
|
413
|
+
.map((arg) => expand(arg, values))
|
|
414
|
+
.map((arg) => SCHEME_URL.exec(arg)?.[2] ?? /^[^@/\s]+@([^:/\s]+):/.exec(arg)?.[1] ?? null);
|
|
415
|
+
const named = remotes.filter((host) => host !== null);
|
|
416
|
+
if (named.some((host) => !isLoopbackName(host))) {
|
|
417
|
+
attempts.push({ program: `git ${subcommand}`, target: after.join(" ") });
|
|
418
|
+
} else if (named.length === 0 && subcommand !== "clone" && after.every((arg) => !/^[./~]/.test(arg))) {
|
|
419
|
+
// A remote by name (origin): whatever it points at is reached.
|
|
420
|
+
attempts.push({ program: `git ${subcommand}`, target: after[0] ?? "the default remote" });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} else if (sealed && (name in PACKAGE_MANAGERS || name in SYSTEM_PACKAGES || isPythonPip(name, args))) {
|
|
424
|
+
const pip = isPythonPip(name, args);
|
|
425
|
+
const effective = pip ? args.slice(args.indexOf("pip") + 1) : args;
|
|
426
|
+
const manager = pip ? "pip" : name;
|
|
427
|
+
const subcommand = effective.find((arg) => !arg.startsWith("-"));
|
|
428
|
+
if (effective.includes("--offline")) continue;
|
|
429
|
+
if (name in SYSTEM_PACKAGES) {
|
|
430
|
+
if (SYSTEM_PACKAGES[name].includes(subcommand)) attempts.push({ program: `${name} ${subcommand}`, target: "" });
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (!PACKAGE_MANAGERS[manager].includes(subcommand)) continue;
|
|
434
|
+
const specs = positionals(effective.slice(effective.indexOf(subcommand) + 1), PACKAGE_VALUE_OPTIONS)
|
|
435
|
+
.map((spec) => expand(spec, values))
|
|
436
|
+
.filter((spec) => !spec.includes("$") && !localSpec(spec));
|
|
437
|
+
if (specs.length > 0) attempts.push({ program: `${manager} ${subcommand}`, target: specs.join(" ") });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Code the command runs that fetches a literal URL: fetch(), http.get(), requests.get(), urlopen(), WebSocket.
|
|
441
|
+
const code =
|
|
442
|
+
/\b(?:fetch|https?\.(?:get|request)|axios(?:\.(?:get|post|put|request))?|got|requests\.(?:get|post|put|head)|urlopen|WebSocket)\(\s*(?:new URL\(\s*)?(["'`])([^"'`]*)\1/g;
|
|
443
|
+
for (const match of command.matchAll(code)) {
|
|
444
|
+
const host = hostOf(match[2].replace(/\$\{[^}]*\}/g, "x"), { bare: false });
|
|
445
|
+
if (host !== null && !isInternalHost(host)) attempts.push({ program: "code", target: match[2] });
|
|
446
|
+
}
|
|
447
|
+
return attempts;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isLoopbackName(host) {
|
|
451
|
+
const name = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
452
|
+
return name === "localhost" || name.endsWith(".localhost") || name === "::1" || /^127(?:\.\d{1,3}){3}$/.test(name);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function isPythonPip(name, args) {
|
|
456
|
+
return /^python(?:3(?:\.\d+)?)?$/.test(name) && args[0] === "-m" && args[1] === "pip";
|
|
457
|
+
}
|