@citeark/agent 0.3.20
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/LICENSE +202 -0
- package/README.md +128 -0
- package/data/dataset-source-registry.v1.json +300 -0
- package/dist/arkgraph/boot.js +6 -0
- package/dist/arkgraph/index.html +1 -0
- package/dist/arkgraph/viewer.css +1 -0
- package/dist/arkgraph/viewer.en.css +1 -0
- package/dist/arkgraph/viewer.en.js +49 -0
- package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
- package/dist/arkgraph/viewer.js +49 -0
- package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
- package/docker/claude-code/Dockerfile +97 -0
- package/docker/claude-code/codex-pro-relay.mjs +466 -0
- package/docker/claude-code/runtime-contract-check.mjs +79 -0
- package/docs/arkgraph-reading.md +79 -0
- package/docs/configuration.md +100 -0
- package/docs/integration.md +92 -0
- package/docs/maturity-plan.md +27 -0
- package/docs/npm-release.md +44 -0
- package/docs/paper-reading.md +40 -0
- package/docs/research-plan-granularity.md +27 -0
- package/docs/terminal.md +49 -0
- package/examples/toy-evaluation/compile-task.json +27 -0
- package/examples/toy-evaluation/paper.md +5 -0
- package/examples/toy-evaluation/repository/README.md +9 -0
- package/examples/toy-evaluation/repository/checkpoint.json +4 -0
- package/examples/toy-evaluation/repository/evaluate.py +17 -0
- package/examples/toy-evaluation/task.json +81 -0
- package/package.json +59 -0
- package/prompts/compile-research.md +58 -0
- package/prompts/execute-contract.md +72 -0
- package/prompts/execute-workspace-simple.md +51 -0
- package/prompts/execute-workspace.md +34 -0
- package/prompts/prepare-reproduction.md +82 -0
- package/prompts/repair-research.md +45 -0
- package/protocol/CAP.md +129 -0
- package/protocol/LICENSE +12 -0
- package/protocol/MAPPINGS.md +72 -0
- package/protocol/README.md +38 -0
- package/protocol/conformance-v2.0-alpha.1.json +36 -0
- package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
- package/protocol/examples/arkgraph/fixtures.mjs +49 -0
- package/protocol/examples/arkgraph/paper-free.json +291 -0
- package/protocol/examples/arkgraph/partial-failure.json +344 -0
- package/protocol/examples/arkgraph/training-evaluation.json +443 -0
- package/protocol/profiles/agent-trace.md +16 -0
- package/protocol/profiles/computational-run.md +16 -0
- package/protocol/profiles/core.md +15 -0
- package/protocol/profiles/public-bundle.md +18 -0
- package/protocol/profiles/reproduction.md +29 -0
- package/protocol/profiles/research-compilation.md +44 -0
- package/protocol/profiles/research-plan.md +39 -0
- package/protocol/profiles/restricted-evidence.md +15 -0
- package/runtime/bootstrap-autodl-runtime.sh +314 -0
- package/runtime/create-runtime-venv.sh +41 -0
- package/runtime/install-local-cpu-runtime.sh +23 -0
- package/runtime/install-scientific-runtime.sh +153 -0
- package/runtime/mineru/parse.py +62 -0
- package/runtime/mineru/requirements.txt +4 -0
- package/runtime/requirements-baseline.txt +38 -0
- package/schemas/cap/v2/activity.schema.json +47 -0
- package/schemas/cap/v2/agent.schema.json +32 -0
- package/schemas/cap/v2/assertion.schema.json +110 -0
- package/schemas/cap/v2/descriptor.schema.json +243 -0
- package/schemas/cap/v2/entity.schema.json +64 -0
- package/schemas/cap/v2/manifest.schema.json +67 -0
- package/schemas/cap/v2/relation.schema.json +82 -0
- package/schemas/compute-catalog.schema.json +63 -0
- package/schemas/compute-decision.schema.json +27 -0
- package/schemas/execution-contract.schema.json +1024 -0
- package/schemas/research-card.schema.json +30 -0
- package/schemas/research-inventory-draft.schema.json +366 -0
- package/schemas/research.schema.json +1044 -0
- package/schemas/result.schema.json +173 -0
- package/schemas/verification-policy.schema.json +47 -0
- package/schemas/verified-conclusion.schema.json +58 -0
- package/schemas/workspace-summary.schema.json +24 -0
- package/scripts/build-arkgraph-view.mjs +12 -0
- package/scripts/check-execution-feasibility.mjs +24 -0
- package/scripts/check-syntax.mjs +15 -0
- package/scripts/deterministic-asset-preparation.py +438 -0
- package/scripts/package-cap.mjs +23 -0
- package/scripts/package-local-agent.mjs +23 -0
- package/scripts/preview-arkgraph.mjs +25 -0
- package/scripts/replay-research-compiler-candidate.mjs +134 -0
- package/scripts/review-compiler-sources.mjs +44 -0
- package/scripts/run-asset-preparation.sh +17 -0
- package/scripts/run-research-plan.mjs +98 -0
- package/scripts/validate-asset-preparation.py +290 -0
- package/scripts/verify-local-runtime.mjs +57 -0
- package/scripts/verify-npm-package.mjs +57 -0
- package/src/adapters/paper2agent.mjs +107 -0
- package/src/assets/cache.mjs +159 -0
- package/src/assets/compute.mjs +98 -0
- package/src/assets/executor.mjs +145 -0
- package/src/assets/lifecycle.mjs +213 -0
- package/src/assets/manifest.mjs +242 -0
- package/src/assets/opportunistic-preparation.mjs +81 -0
- package/src/assets/plan.mjs +411 -0
- package/src/assets/prompts.mjs +29 -0
- package/src/assets/public-asset-probe.mjs +525 -0
- package/src/assets/qualification.mjs +119 -0
- package/src/assets/readiness.mjs +130 -0
- package/src/assets/reproduction-admission.mjs +355 -0
- package/src/assets/requirements.mjs +152 -0
- package/src/assets/source-grounding.mjs +341 -0
- package/src/assets/source-policy.mjs +118 -0
- package/src/autodl/client.mjs +260 -0
- package/src/autodl/ssh.mjs +380 -0
- package/src/autodl/tools.mjs +129 -0
- package/src/cap/redaction.mjs +38 -0
- package/src/cap/v2/archive.mjs +152 -0
- package/src/cap/v2/attestation.mjs +204 -0
- package/src/cap/v2/canonical-json.mjs +114 -0
- package/src/cap/v2/compilation-artifact.mjs +240 -0
- package/src/cap/v2/core.mjs +282 -0
- package/src/cap/v2/measurement-assessment-records.mjs +23 -0
- package/src/cap/v2/pipeline-artifact.mjs +922 -0
- package/src/cap/v2/read.mjs +41 -0
- package/src/cap/v2/reassessment-artifact.mjs +383 -0
- package/src/cap/v2/research-artifact.mjs +231 -0
- package/src/cap/v2/research-map-records.mjs +46 -0
- package/src/cap/v2/research-object-records.mjs +163 -0
- package/src/cap/v2/research-records.mjs +187 -0
- package/src/cap/v2/verify.mjs +642 -0
- package/src/cli.mjs +1146 -0
- package/src/compute/autodl-pro-compiler.mjs +347 -0
- package/src/compute/autodl-pro-executor.mjs +459 -0
- package/src/compute/autodl-pro-job.mjs +843 -0
- package/src/compute/autodl-pro-network.mjs +295 -0
- package/src/compute/autodl-pro-remote.mjs +810 -0
- package/src/compute/autodl-pro-staging.mjs +117 -0
- package/src/compute/campaign.mjs +110 -0
- package/src/compute/catalog.mjs +123 -0
- package/src/compute/checkpoint-protocol.mjs +154 -0
- package/src/compute/codex-account-lock.mjs +111 -0
- package/src/compute/codex-account-session.mjs +107 -0
- package/src/compute/compiler-profile.mjs +38 -0
- package/src/compute/compiler-router.mjs +23 -0
- package/src/compute/coordinator-recovery.mjs +210 -0
- package/src/compute/executor-router.mjs +29 -0
- package/src/compute/gcp-batch-compiler.mjs +685 -0
- package/src/compute/gcp-batch-executor.mjs +1215 -0
- package/src/compute/gcp-batch-failure.mjs +92 -0
- package/src/compute/gcp-batch-job.mjs +527 -0
- package/src/compute/gcp-batch-lifecycle.mjs +81 -0
- package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
- package/src/compute/local-codex-compiler.mjs +52 -0
- package/src/compute/measurement-hardware.mjs +128 -0
- package/src/compute/remote-attempt.mjs +226 -0
- package/src/compute/requirements.mjs +124 -0
- package/src/compute/research-phases.mjs +48 -0
- package/src/compute/scheduler.mjs +452 -0
- package/src/compute/shared-workloads.mjs +26 -0
- package/src/compute/stage-archive.mjs +79 -0
- package/src/contracts/campaign-contract.mjs +52 -0
- package/src/contracts/execution-contract.mjs +819 -0
- package/src/contracts/execution-mode.mjs +19 -0
- package/src/contracts/execution-timeouts.mjs +45 -0
- package/src/contracts/execution-workload.mjs +68 -0
- package/src/contracts/preflight-schema.mjs +25 -0
- package/src/contracts/public-contract.mjs +63 -0
- package/src/contracts/subject-tags.mjs +31 -0
- package/src/dashboard/data.mjs +898 -0
- package/src/dashboard/server.mjs +79 -0
- package/src/dashboard/static/dashboard.css +366 -0
- package/src/dashboard/static/dashboard.js +560 -0
- package/src/dashboard/static/index.html +85 -0
- package/src/deployment/community-policy.mjs +9 -0
- package/src/deployment/environment.mjs +112 -0
- package/src/deployment/guided.mjs +98 -0
- package/src/deployment/handoff.mjs +102 -0
- package/src/deployment/local-contract.mjs +31 -0
- package/src/deployment/local.mjs +100 -0
- package/src/deployment/prepare.mjs +46 -0
- package/src/deployment/recipe.mjs +108 -0
- package/src/deployment/supplement.mjs +51 -0
- package/src/deployment/terminal.mjs +43 -0
- package/src/diagnosis/renderer.mjs +75 -0
- package/src/diagnosis/target-failure.mjs +46 -0
- package/src/evidence/parser-registry.mjs +54 -0
- package/src/evidence/parsers/fasttext-classification.mjs +82 -0
- package/src/evidence/parsers/json-scalar.mjs +96 -0
- package/src/evidence/parsers/simcse-senteval.mjs +104 -0
- package/src/evidence/parsers/starspace-classification.mjs +78 -0
- package/src/evidence/registry.mjs +147 -0
- package/src/execution/runner-audit.mjs +473 -0
- package/src/gcp/auth.mjs +106 -0
- package/src/gcp/batch-client.mjs +120 -0
- package/src/gcp/resource-discovery.mjs +177 -0
- package/src/gcp/rest.mjs +82 -0
- package/src/gcp/secret-manager.mjs +34 -0
- package/src/gcp/signed-url.mjs +133 -0
- package/src/gcp/storage.mjs +220 -0
- package/src/graph/command.mjs +41 -0
- package/src/graph/execution.mjs +97 -0
- package/src/graph/model.mjs +37 -0
- package/src/graph/presentation.mjs +110 -0
- package/src/graph/query.mjs +159 -0
- package/src/graph/research-relations.mjs +69 -0
- package/src/graph/source-page.mjs +12 -0
- package/src/graph/source-preview.mjs +34 -0
- package/src/graph/validate.mjs +76 -0
- package/src/job.mjs +496 -0
- package/src/network/autodl-routing-proxy.mjs +462 -0
- package/src/network/egress-proxy.mjs +158 -0
- package/src/observability/event-contract.mjs +230 -0
- package/src/observability/pipeline-monitor.mjs +166 -0
- package/src/pipeline/orchestrator.mjs +1281 -0
- package/src/pipeline/recovery-error.mjs +11 -0
- package/src/pipeline/replay.mjs +304 -0
- package/src/pipeline/shared-execution.mjs +115 -0
- package/src/pipeline/stage-checkpoint.mjs +86 -0
- package/src/pipeline/stage-recovery.mjs +101 -0
- package/src/pipeline/targets.mjs +110 -0
- package/src/process.mjs +143 -0
- package/src/protocol.mjs +312 -0
- package/src/provider/codex-account.mjs +44 -0
- package/src/provider/codex-completion.mjs +49 -0
- package/src/provider/completion.mjs +292 -0
- package/src/provider/model-client.mjs +44 -0
- package/src/provider/model-route.mjs +29 -0
- package/src/provider/openrouter-readiness.mjs +189 -0
- package/src/provider/reader-bridge.mjs +35 -0
- package/src/provider/relay.mjs +263 -0
- package/src/provider/runtime-auth.mjs +40 -0
- package/src/public/cap.d.mts +90 -0
- package/src/public/cap.mjs +12 -0
- package/src/public/contracts.d.mts +2 -0
- package/src/public/host.mjs +171 -0
- package/src/public/operations.d.mts +11 -0
- package/src/public/presentation.d.mts +4 -0
- package/src/records/views.mjs +26 -0
- package/src/remote/command.mjs +178 -0
- package/src/remote/ssh.mjs +59 -0
- package/src/repository-origin.mjs +81 -0
- package/src/reproduction/evidence-feedback.mjs +96 -0
- package/src/reproduction/incomplete-initialization.mjs +25 -0
- package/src/reproduction/lifecycle.mjs +253 -0
- package/src/reproduction/plan.mjs +132 -0
- package/src/reproduction/prompts.mjs +70 -0
- package/src/reproduction/runner.mjs +188 -0
- package/src/reproduction/summary.mjs +130 -0
- package/src/reproduction/workspace-mode.mjs +7 -0
- package/src/research/automatic-admission.mjs +156 -0
- package/src/research/compiler-coverage.mjs +85 -0
- package/src/research/compiler-failure.mjs +24 -0
- package/src/research/compiler-normalization-guards.mjs +112 -0
- package/src/research/compiler-repair.mjs +3 -0
- package/src/research/compiler.mjs +853 -0
- package/src/research/continuation-selection.mjs +26 -0
- package/src/research/execution-graph-context.mjs +43 -0
- package/src/research/experiment-importance.mjs +15 -0
- package/src/research/inventory-handoff.mjs +104 -0
- package/src/research/inventory-revisions.mjs +32 -0
- package/src/research/mineru-local.mjs +73 -0
- package/src/research/paper-command.mjs +19 -0
- package/src/research/paper-markdown.mjs +180 -0
- package/src/research/paper-source-map.mjs +69 -0
- package/src/research/planning-policy.mjs +88 -0
- package/src/research/reference-materials.mjs +11 -0
- package/src/research/reproduction-scope.mjs +30 -0
- package/src/research/research-map.mjs +94 -0
- package/src/research/research-objects.mjs +88 -0
- package/src/research/source-discovery.mjs +646 -0
- package/src/research/source-observations.mjs +75 -0
- package/src/research/source-review-cli-mcp.mjs +26 -0
- package/src/research/source-review-input.mjs +209 -0
- package/src/research/source-review-local-codex.mjs +36 -0
- package/src/research/source-review-model.mjs +70 -0
- package/src/research/source-review.mjs +173 -0
- package/src/research/structure.mjs +3163 -0
- package/src/research-card/renderer.mjs +277 -0
- package/src/research-card/verified-conclusion.mjs +143 -0
- package/src/results/output-registry.mjs +183 -0
- package/src/runtime/claude-code.mjs +52 -0
- package/src/runtime/codex-capacity-retry.mjs +87 -0
- package/src/runtime/codex.mjs +64 -0
- package/src/runtime/config.mjs +157 -0
- package/src/runtime/final-output.mjs +40 -0
- package/src/runtime/index.mjs +21 -0
- package/src/runtime/local-codex.mjs +74 -0
- package/src/runtime/opencode.mjs +95 -0
- package/src/runtime/prompt.mjs +13 -0
- package/src/sandbox/docker.mjs +363 -0
- package/src/settings/command.mjs +297 -0
- package/src/settings/store.mjs +119 -0
- package/src/telemetry/pricing.mjs +68 -0
- package/src/telemetry/usage.mjs +265 -0
- package/src/terminal/events.mjs +97 -0
- package/src/terminal/input.mjs +40 -0
- package/src/terminal/plain.mjs +40 -0
- package/src/terminal/remote-stream.mjs +22 -0
- package/src/terminal/screen.mjs +214 -0
- package/src/terminal/transcript.mjs +69 -0
- package/src/util.mjs +107 -0
- package/src/verification/ai-assessor.mjs +534 -0
- package/src/verification/claim-evaluator.mjs +242 -0
- package/src/verification/evidence-context.mjs +165 -0
- package/src/verification/evidence-reader.mjs +95 -0
- package/src/verification/integrity.mjs +570 -0
- package/src/verification/tolerance.mjs +32 -0
- package/src/workloads/cpu-research-preparation.mjs +56 -0
- package/src/workloads/definition.mjs +74 -0
- package/src/workloads/phase-aware-reproduction.mjs +46 -0
- package/src/workloads/reproduction.mjs +85 -0
- package/src/workspace/command.mjs +242 -0
- package/src/workspace/control.mjs +49 -0
- package/src/workspace/entry.mjs +28 -0
- package/src/workspace/input.mjs +93 -0
- package/src/workspace/interactive.mjs +94 -0
- package/src/workspace/jobs.mjs +418 -0
- package/src/workspace/session.mjs +97 -0
- package/src/workspace/worker.mjs +137 -0
- package/ui/arkgraph/ambient-motion.mjs +10 -0
- package/ui/arkgraph/app.jsx +153 -0
- package/ui/arkgraph/boot.js +6 -0
- package/ui/arkgraph/camera-motion.mjs +20 -0
- package/ui/arkgraph/context-reveal.mjs +39 -0
- package/ui/arkgraph/details.css +3 -0
- package/ui/arkgraph/entry.jsx +28 -0
- package/ui/arkgraph/experiment-curves.mjs +17 -0
- package/ui/arkgraph/experiment-selection.mjs +15 -0
- package/ui/arkgraph/experiment-style.css +26 -0
- package/ui/arkgraph/experiment-ui.jsx +32 -0
- package/ui/arkgraph/frame.html +1 -0
- package/ui/arkgraph/graph-gestures.mjs +62 -0
- package/ui/arkgraph/label-layout.mjs +57 -0
- package/ui/arkgraph/locales/en.json +229 -0
- package/ui/arkgraph/locales/source-types.json +15 -0
- package/ui/arkgraph/localization-build.mjs +27 -0
- package/ui/arkgraph/material-build.mjs +23 -0
- package/ui/arkgraph/material-colors.mjs +39 -0
- package/ui/arkgraph/material-style.css +15 -0
- package/ui/arkgraph/open-graph.jsx +326 -0
- package/ui/arkgraph/outline.jsx +49 -0
- package/ui/arkgraph/package-lock.json +888 -0
- package/ui/arkgraph/package.json +17 -0
- package/ui/arkgraph/reading-layout.mjs +130 -0
- package/ui/arkgraph/reading-presentation.mjs +73 -0
- package/ui/arkgraph/record-detail.css +51 -0
- package/ui/arkgraph/record-details.jsx +29 -0
- package/ui/arkgraph/research-types.mjs +31 -0
- package/ui/arkgraph/selection-mark.jsx +6 -0
- package/ui/arkgraph/soft-spine.mjs +26 -0
- package/ui/arkgraph/steering-style.css +187 -0
- package/ui/arkgraph/style.css +272 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Default is local input preparation only. --apply sends fixed sources and
|
|
2
|
+
// candidates to the configured OpenRouter model using the supplied budget.
|
|
3
|
+
import { readFile, mkdir } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { prepareSourceReviewInput, reviewCompilerSources } from '../src/research/source-review.mjs';
|
|
6
|
+
import { writeJson } from '../src/util.mjs';
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const value = (flag) => { const i = args.indexOf(flag); return i < 0 ? null : args[i + 1]; };
|
|
9
|
+
const candidates = args.flatMap((arg, i) => arg === '--candidate' ? [args[i + 1]] : []);
|
|
10
|
+
const paperPath = value('--paper');
|
|
11
|
+
const repositoryRoot = value('--repository');
|
|
12
|
+
const output = value('--output');
|
|
13
|
+
if (!paperPath || !repositoryRoot || !output || !candidates.length) throw new Error('Required: --paper FILE --repository SNAPSHOT --output DIR --candidate FILE (repeatable). Default: no model calls.');
|
|
14
|
+
await mkdir(output, { recursive: true });
|
|
15
|
+
const sources = await prepareSourceReviewInput({ paperPath, repositoryRoot, directory: path.join(output, 'source-review') });
|
|
16
|
+
const manifest = { ...sources.identity, pageCount: sources.pages.length, initialImages: 0,
|
|
17
|
+
repositoryFiles: sources.repositoryIndex, initialRepositoryContentBytes: 0,
|
|
18
|
+
orientationBytes: Buffer.byteLength(JSON.stringify(sources.orientation)),
|
|
19
|
+
sourceIndexBytes: Buffer.byteLength(JSON.stringify({pages:sources.pageIndex,repository:sources.repositoryIndex})),
|
|
20
|
+
candidateFiles: candidates.map((filename) => path.resolve(filename)), applied: args.includes('--apply') };
|
|
21
|
+
await writeJson(path.join(output, 'input-manifest.json'), manifest);
|
|
22
|
+
if (!args.includes('--apply')) { console.log(JSON.stringify({ status: 'prepared_only', pageCount: manifest.pageCount, files: manifest.repositoryFiles.length, orientationBytes: manifest.orientationBytes, sourceIndexBytes: manifest.sourceIndexBytes })); }
|
|
23
|
+
else {
|
|
24
|
+
if (!value('--runtime-json') || !value('--budget-json')) throw new Error('--apply requires --runtime-json and --budget-json; no unmetered fallback');
|
|
25
|
+
const runtimeAgent = JSON.parse(await readFile(value('--runtime-json'), 'utf8'));
|
|
26
|
+
runtimeAgent.paperBudgetEnforced = true;
|
|
27
|
+
const paperBudget = JSON.parse(await readFile(value('--budget-json'), 'utf8'));
|
|
28
|
+
if (!paperBudget.url || !paperBudget.token || !paperBudget.workerId) throw new Error('Budget descriptor must include url, token and workerId');
|
|
29
|
+
const bundle = { directories: { execution: output, repositorySnapshot: repositoryRoot }, runtimeAgent, paperBudget,
|
|
30
|
+
sourceReviewProviderSecret: value('--provider-secret'), runtimeTask: { paper: { digest: `sha256:${sources.identity.paperDigest}` }, reproductionScope: value('--scope') ?? 'low' } };
|
|
31
|
+
const results = [];
|
|
32
|
+
for (const filename of candidates) {
|
|
33
|
+
const candidate = JSON.parse(await readFile(filename, 'utf8'));
|
|
34
|
+
if (candidate.provenance) delete candidate.provenance.compilerSourceReview;
|
|
35
|
+
try { const review = await reviewCompilerSources({ bundle, candidate, paperPath, prepare: async () => sources }); results.push({ candidate: path.resolve(filename), accepted: true, reportDigest: review.reportDigest }); }
|
|
36
|
+
catch (error) {
|
|
37
|
+
results.push({ candidate: path.resolve(filename), accepted: false, code: error.failureCode, message: error.message });
|
|
38
|
+
if (error.failureCode !== 'compiler.source_review_rejected') break;
|
|
39
|
+
}
|
|
40
|
+
await writeJson(path.join(output, 'results.json'), results);
|
|
41
|
+
}
|
|
42
|
+
await writeJson(path.join(output, 'results.json'), results);
|
|
43
|
+
console.log(JSON.stringify(results));
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
deterministic_script="$1"
|
|
5
|
+
shift
|
|
6
|
+
set +e
|
|
7
|
+
python3 "$deterministic_script"
|
|
8
|
+
status=$?
|
|
9
|
+
set -e
|
|
10
|
+
if [ "$status" -eq 0 ]; then
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
if [ "$status" -ne 86 ]; then
|
|
14
|
+
exit "$status"
|
|
15
|
+
fi
|
|
16
|
+
printf '%s\n' '{"type":"citeark.asset_preparation_agent_fallback","reason":"deterministic-fast-path-declined"}'
|
|
17
|
+
exec "$@"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The staged helper imports the same plan validator as the coordinator.
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { createWriteStream } from "node:fs";
|
|
4
|
+
import { appendFile, mkdir, open, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { finished } from "node:stream/promises";
|
|
8
|
+
import { resolveResearchPlan } from "../src/reproduction/plan.mjs";
|
|
9
|
+
import { inspectResearchPlanEvidence } from "../src/reproduction/evidence-feedback.mjs";
|
|
10
|
+
|
|
11
|
+
const [planPath = "/job/output/research-plan.json", ...args] = process.argv.slice(2);
|
|
12
|
+
const scientificStepId = args[0] === '--step' ? args[1] : null;
|
|
13
|
+
const command = args[0] === '--step' ? args.slice(2) : args;
|
|
14
|
+
const contractPath = path.resolve(path.dirname(planPath), "../input/task.json");
|
|
15
|
+
const contract = JSON.parse(await readFile(contractPath, "utf8"));
|
|
16
|
+
let resolved;
|
|
17
|
+
try {
|
|
18
|
+
if (args[0] === '--step' && (!scientificStepId || !contract.researchGraph?.steps?.some(step => step.id === scientificStepId))) {
|
|
19
|
+
throw new Error(`Unknown scientific step: ${String(scientificStepId)}`);
|
|
20
|
+
}
|
|
21
|
+
resolved = await resolveResearchPlan(contract, path.dirname(planPath));
|
|
22
|
+
if (!resolved.executionPlan || path.basename(planPath) !== "research-plan.json") {
|
|
23
|
+
throw new Error("Expected research-plan.json for a workspacePlan-enabled task");
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`${error.message}\nCorrect the plan and run this helper again. Reuse existing raw evidence; plan validation does not require repeating the experiment.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const plan = resolved.executionPlan.plan;
|
|
30
|
+
const canonical = (value) => Array.isArray(value) ? value.map(canonical)
|
|
31
|
+
: value && typeof value === "object"
|
|
32
|
+
? Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])])) : value;
|
|
33
|
+
const bytes = JSON.stringify(canonical(plan));
|
|
34
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
35
|
+
const directory = path.join(path.dirname(planPath), "plan-history");
|
|
36
|
+
await mkdir(directory, { recursive: true });
|
|
37
|
+
let newRevision = true;
|
|
38
|
+
try { await writeFile(path.join(directory, `${digest}.json`), bytes, { flag: "wx" }); }
|
|
39
|
+
catch (error) { if (error.code !== "EEXIST") throw error; newRevision = false; }
|
|
40
|
+
// The runner captures this statement and its exact plan bytes outside the
|
|
41
|
+
// workspace. Snapshot files alone are Agent declarations, not execution proof.
|
|
42
|
+
// Always recapture complete final bytes. Repeated commands with an unchanged
|
|
43
|
+
// plan can refer to the revision, avoiding the same large snapshot on every probe.
|
|
44
|
+
console.log(JSON.stringify({ event: "research_plan_execution", planDigest: `sha256:${digest}`,
|
|
45
|
+
...(newRevision || !command.length ? { plan } : {}), command,
|
|
46
|
+
...(!command.length && contract.workspacePlan?.version === 2
|
|
47
|
+
? { evidenceFeedback: await inspectResearchPlanEvidence(resolved, path.dirname(planPath)) } : {}),
|
|
48
|
+
}));
|
|
49
|
+
if (command.length) {
|
|
50
|
+
const startedAt = new Date().toISOString();
|
|
51
|
+
const capture = contract.workspacePlan?.version === 2;
|
|
52
|
+
const executionId = randomUUID();
|
|
53
|
+
const stdoutPath = `experiment-logs/${executionId}.stdout.log`;
|
|
54
|
+
const stderrPath = `experiment-logs/${executionId}.stderr.log`;
|
|
55
|
+
const record = { planDigest: `sha256:${digest}`, command, startedAt,
|
|
56
|
+
...(scientificStepId ? { scientificStepId } : {}),
|
|
57
|
+
...(capture ? { executionId, stdoutPath, stderrPath } : {}) };
|
|
58
|
+
const recordPath = path.join(path.dirname(planPath), "experiment-records.jsonl");
|
|
59
|
+
const recordEvent = async (event) => {
|
|
60
|
+
if (contract.workspacePlan?.version !== 2) return;
|
|
61
|
+
const line = JSON.stringify({ ...record, ...event });
|
|
62
|
+
await appendFile(recordPath, `${line}\n`);
|
|
63
|
+
console.log(line);
|
|
64
|
+
};
|
|
65
|
+
const logs = [];
|
|
66
|
+
if (capture) {
|
|
67
|
+
await mkdir(path.join(path.dirname(planPath), "experiment-logs"), { recursive: true });
|
|
68
|
+
for (const filename of [stdoutPath, stderrPath]) {
|
|
69
|
+
const target = path.join(path.dirname(planPath), filename);
|
|
70
|
+
// Open before starting the command: a logging failure must not silently
|
|
71
|
+
// discard a scientific run. Each invocation gets fresh files.
|
|
72
|
+
const handle = await open(target, "wx");
|
|
73
|
+
const stream = createWriteStream(target, { fd: handle.fd, autoClose: false });
|
|
74
|
+
logs.push({ handle, stream });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
await recordEvent({ event: "experiment_started" });
|
|
78
|
+
const child = spawn(command[0], command.slice(1), { stdio: capture ? ["inherit", "pipe", "pipe"] : "inherit" });
|
|
79
|
+
const written = logs.map(({ stream }) => finished(stream));
|
|
80
|
+
if (capture) {
|
|
81
|
+
child.stdout.pipe(logs[0].stream);
|
|
82
|
+
child.stderr.pipe(logs[1].stream);
|
|
83
|
+
child.stdout.pipe(process.stdout, { end: false });
|
|
84
|
+
child.stderr.pipe(process.stderr, { end: false });
|
|
85
|
+
}
|
|
86
|
+
let spawnError = null;
|
|
87
|
+
child.on("error", (error) => { spawnError = error.message; console.error(error.message); });
|
|
88
|
+
const [[code, signal]] = await Promise.all([
|
|
89
|
+
new Promise((resolve) => child.once("close", (...args) => resolve(args))),
|
|
90
|
+
...written,
|
|
91
|
+
]);
|
|
92
|
+
for (const { handle } of logs) { await handle.sync(); await handle.close(); }
|
|
93
|
+
process.exitCode = spawnError ? 1 : code ?? (signal ? 1 : 0);
|
|
94
|
+
await recordEvent({ event: "experiment_finished", finishedAt: new Date().toISOString(),
|
|
95
|
+
exitCode: process.exitCode, signal, ...(spawnError ? { error: spawnError } : {}),
|
|
96
|
+
...(capture ? { evidenceFeedback: await inspectResearchPlanEvidence(resolved, path.dirname(planPath)) } : {}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Deterministically validate and finalize an asset-preparation manifest."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import fnmatch
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
JOB_ROOT = Path(os.environ.get("CITEARK_JOB_ROOT", "/job")).resolve()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def canonical(value: object) -> bytes:
|
|
19
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def digest_bytes(value: bytes) -> str:
|
|
23
|
+
return "sha256:" + hashlib.sha256(value).hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def digest_file(target: Path) -> str:
|
|
27
|
+
digest = hashlib.sha256()
|
|
28
|
+
with target.open("rb") as handle:
|
|
29
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
30
|
+
digest.update(chunk)
|
|
31
|
+
return "sha256:" + digest.hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def inside_job(value: str, *, allow_final_symlink: bool = False) -> Path:
|
|
35
|
+
target = Path(value)
|
|
36
|
+
if target.is_absolute() and target.parts[:2] == ("/", "job") and JOB_ROOT != Path("/job"):
|
|
37
|
+
target = JOB_ROOT.joinpath(*target.parts[2:])
|
|
38
|
+
elif not target.is_absolute():
|
|
39
|
+
target = JOB_ROOT / target
|
|
40
|
+
# Task-local virtual environments intentionally use a python symlink into
|
|
41
|
+
# the immutable base image. Permit that final executable symlink only for
|
|
42
|
+
# the explicit runtime probe; all evidence and asset paths still resolve
|
|
43
|
+
# their complete chain before the containment check.
|
|
44
|
+
resolved = Path(os.path.abspath(target)) if allow_final_symlink else target.resolve()
|
|
45
|
+
if resolved != JOB_ROOT and JOB_ROOT not in resolved.parents:
|
|
46
|
+
raise ValueError(f"path escapes /job: {value}")
|
|
47
|
+
return resolved
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def relative_output(value: str) -> Path:
|
|
51
|
+
if not value or value.startswith("/") or ".." in Path(value).parts:
|
|
52
|
+
raise ValueError(f"unsafe evidence path: {value}")
|
|
53
|
+
return inside_job(f"output/{value}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def inventory_asset(asset: dict, record: dict) -> None:
|
|
57
|
+
root = inside_job(asset["localPath"])
|
|
58
|
+
# localPath is a host-owned binding. A model may restate it incorrectly in
|
|
59
|
+
# the draft manifest, but it is never allowed to redirect prepared bytes
|
|
60
|
+
# away from the immutable plan. Canonicalize, then inspect the planned path.
|
|
61
|
+
record["localPath"] = asset["localPath"]
|
|
62
|
+
if record.get("status") not in {"prepared", "stream_ready"}:
|
|
63
|
+
return
|
|
64
|
+
if not root.is_dir():
|
|
65
|
+
raise ValueError(f"prepared asset directory missing: {root}")
|
|
66
|
+
files = []
|
|
67
|
+
for target in sorted(root.rglob("*")):
|
|
68
|
+
if target.is_symlink():
|
|
69
|
+
raise ValueError(f"asset symlink is not allowed: {target}")
|
|
70
|
+
if not target.is_file():
|
|
71
|
+
continue
|
|
72
|
+
relative = target.relative_to(root).as_posix()
|
|
73
|
+
files.append({
|
|
74
|
+
"path": relative,
|
|
75
|
+
"bytes": target.stat().st_size,
|
|
76
|
+
"digest": digest_file(target),
|
|
77
|
+
})
|
|
78
|
+
if not files:
|
|
79
|
+
raise ValueError(f"prepared asset has no files: {asset['id']}")
|
|
80
|
+
total = sum(item["bytes"] for item in files)
|
|
81
|
+
if total > asset["maximumExpandedBytes"]:
|
|
82
|
+
raise ValueError(f"asset exceeds approved disk budget: {asset['id']}")
|
|
83
|
+
paths = [item["path"] for item in files]
|
|
84
|
+
requested = asset.get("requiredPaths", [])
|
|
85
|
+
record["selectionObservation"] = {
|
|
86
|
+
"requested": requested,
|
|
87
|
+
"matched": [
|
|
88
|
+
pattern for pattern in requested
|
|
89
|
+
if any(fnmatch.fnmatchcase(candidate, pattern) for candidate in paths)
|
|
90
|
+
],
|
|
91
|
+
"unmatched": [
|
|
92
|
+
pattern for pattern in requested
|
|
93
|
+
if not any(fnmatch.fnmatchcase(candidate, pattern) for candidate in paths)
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
for pattern in asset.get("excludedPaths", []):
|
|
97
|
+
if any(fnmatch.fnmatchcase(candidate, pattern) for candidate in paths):
|
|
98
|
+
raise ValueError(f"asset contains excluded path {pattern}: {asset['id']}")
|
|
99
|
+
record["files"] = files
|
|
100
|
+
record["totalBytes"] = total
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def validate_evidence(check: dict, record: dict, *, require_passed: bool = True) -> None:
|
|
104
|
+
if require_passed and record.get("status") in {"verified", "passed"}:
|
|
105
|
+
record["status"] = "passed"
|
|
106
|
+
if require_passed and record.get("status") != "passed":
|
|
107
|
+
raise ValueError(f"real preflight did not pass: {check['id']}")
|
|
108
|
+
commands = record.get("commands")
|
|
109
|
+
if not isinstance(commands, list) or not any(isinstance(item, str) and item.strip() for item in commands):
|
|
110
|
+
raise ValueError(f"preflight command missing: {check['id']}")
|
|
111
|
+
descriptors = record.get("evidence")
|
|
112
|
+
if not isinstance(descriptors, list) or not descriptors:
|
|
113
|
+
raise ValueError(f"preflight evidence missing: {check['id']}")
|
|
114
|
+
normalized_descriptors = []
|
|
115
|
+
for descriptor in descriptors:
|
|
116
|
+
if isinstance(descriptor, str):
|
|
117
|
+
descriptor = {"path": descriptor}
|
|
118
|
+
if not isinstance(descriptor, dict):
|
|
119
|
+
raise ValueError(f"preflight evidence descriptor invalid: {check['id']}")
|
|
120
|
+
target = relative_output(descriptor.get("path", ""))
|
|
121
|
+
if not target.is_file():
|
|
122
|
+
raise ValueError(f"preflight evidence file missing: {target}")
|
|
123
|
+
descriptor["bytes"] = target.stat().st_size
|
|
124
|
+
descriptor["digest"] = digest_file(target)
|
|
125
|
+
normalized_descriptors.append(descriptor)
|
|
126
|
+
record["evidence"] = normalized_descriptors
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_asset_identity(asset: dict, record: dict) -> None:
|
|
130
|
+
if record.get("identity") != asset.get("identity"):
|
|
131
|
+
raise ValueError(f"asset identity mismatch: {asset['id']}")
|
|
132
|
+
if asset.get("revision") and record.get("revision") != asset.get("revision"):
|
|
133
|
+
raise ValueError(f"asset revision mismatch: {asset['id']}")
|
|
134
|
+
allowed_sources = {
|
|
135
|
+
item.get("url") for item in asset.get("sourceLocators", []) if isinstance(item, dict)
|
|
136
|
+
}
|
|
137
|
+
if allowed_sources and record.get("sourceLocator") not in allowed_sources:
|
|
138
|
+
raise ValueError(f"asset source is outside the approved plan: {asset['id']}")
|
|
139
|
+
verification = record.get("verification")
|
|
140
|
+
if not isinstance(verification, dict):
|
|
141
|
+
raise ValueError(f"asset identity verification missing: {asset['id']}")
|
|
142
|
+
validate_evidence({"id": f"asset:{asset['id']}"}, verification)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def validate_acquisition(asset: dict, record: dict) -> int:
|
|
146
|
+
acquisition = record.get("acquisition")
|
|
147
|
+
if not isinstance(acquisition, dict):
|
|
148
|
+
raise ValueError(f"asset transfer audit missing: {asset['id']}")
|
|
149
|
+
download_bytes = acquisition.get("downloadBytes")
|
|
150
|
+
if not isinstance(download_bytes, int) or isinstance(download_bytes, bool) or download_bytes < 0:
|
|
151
|
+
raise ValueError(f"asset download bytes invalid: {asset['id']}")
|
|
152
|
+
if download_bytes > asset["maximumDownloadBytes"]:
|
|
153
|
+
raise ValueError(f"asset exceeds approved download budget: {asset['id']}")
|
|
154
|
+
locators = asset.get("sourceLocators", [])
|
|
155
|
+
selected = next(
|
|
156
|
+
(item for item in locators if item.get("url") == record.get("sourceLocator")),
|
|
157
|
+
None,
|
|
158
|
+
)
|
|
159
|
+
if selected:
|
|
160
|
+
expected_digest = selected.get("expectedDigest")
|
|
161
|
+
if expected_digest and acquisition.get("sourceDigest") != expected_digest:
|
|
162
|
+
raise ValueError(f"asset source digest mismatch: {asset['id']}")
|
|
163
|
+
if selected.get("fallbackRequiresDigestMatch") and not expected_digest:
|
|
164
|
+
raise ValueError(f"unverified mirror is not an acceptable fallback: {asset['id']}")
|
|
165
|
+
earlier = [
|
|
166
|
+
item for item in locators
|
|
167
|
+
if item.get("rank", 999999) < selected.get("rank", 999999)
|
|
168
|
+
]
|
|
169
|
+
attempts = {
|
|
170
|
+
item.get("url"): item
|
|
171
|
+
for item in acquisition.get("attemptedSources", [])
|
|
172
|
+
if isinstance(item, dict)
|
|
173
|
+
}
|
|
174
|
+
for candidate in earlier:
|
|
175
|
+
if attempts.get(candidate.get("url"), {}).get("status") != "failed":
|
|
176
|
+
raise ValueError(
|
|
177
|
+
f"higher-priority source failure was not recorded: {asset['id']}"
|
|
178
|
+
)
|
|
179
|
+
validate_evidence(
|
|
180
|
+
{"id": f"acquisition:{asset['id']}"},
|
|
181
|
+
acquisition,
|
|
182
|
+
require_passed=False,
|
|
183
|
+
)
|
|
184
|
+
return download_bytes
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def runtime_environment() -> dict:
|
|
188
|
+
python = inside_job("runtime-venv/bin/python", allow_final_symlink=True)
|
|
189
|
+
if not python.is_file():
|
|
190
|
+
raise ValueError("task-local /job/runtime-venv Python is missing")
|
|
191
|
+
version = subprocess.run(
|
|
192
|
+
[str(python), "--version"], check=True, capture_output=True, text=True
|
|
193
|
+
).stdout.strip()
|
|
194
|
+
if not version:
|
|
195
|
+
version = subprocess.run(
|
|
196
|
+
[str(python), "--version"], check=True, capture_output=True, text=True
|
|
197
|
+
).stderr.strip()
|
|
198
|
+
packages = subprocess.run(
|
|
199
|
+
[str(python), "-m", "pip", "freeze", "--all"],
|
|
200
|
+
check=True,
|
|
201
|
+
capture_output=True,
|
|
202
|
+
text=True,
|
|
203
|
+
).stdout
|
|
204
|
+
normalized = "\n".join(sorted(line.strip() for line in packages.splitlines() if line.strip())) + "\n"
|
|
205
|
+
target = inside_job("output/logs/asset-preparation/package-inventory.txt")
|
|
206
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
target.write_text(normalized, encoding="utf-8")
|
|
208
|
+
inventory_digest = digest_bytes(normalized.encode())
|
|
209
|
+
return {
|
|
210
|
+
"path": "/job/runtime-venv",
|
|
211
|
+
"pythonVersion": version,
|
|
212
|
+
"lockPath": "logs/asset-preparation/package-inventory.txt",
|
|
213
|
+
"lockDigest": inventory_digest,
|
|
214
|
+
"packageInventoryDigest": inventory_digest,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main() -> int:
|
|
219
|
+
if len(sys.argv) != 3:
|
|
220
|
+
raise ValueError("usage: validate-asset-preparation.py PLAN MANIFEST")
|
|
221
|
+
plan = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
222
|
+
manifest_path = Path(sys.argv[2])
|
|
223
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
224
|
+
if manifest.get("schemaVersion") != "0.1" or manifest.get("kind") != "citeark.asset-preparation-manifest":
|
|
225
|
+
raise ValueError("invalid manifest envelope")
|
|
226
|
+
if manifest.get("planDigest") != plan.get("planDigest"):
|
|
227
|
+
raise ValueError("manifest plan digest mismatch")
|
|
228
|
+
records = {item.get("assetId"): item for item in manifest.get("assets", []) if isinstance(item, dict)}
|
|
229
|
+
if len(records) != len(manifest.get("assets", [])):
|
|
230
|
+
raise ValueError("asset manifest contains duplicate or invalid records")
|
|
231
|
+
planned_asset_ids = {asset["id"] for asset in plan.get("assets", [])}
|
|
232
|
+
if set(records) != planned_asset_ids:
|
|
233
|
+
raise ValueError("asset manifest records do not exactly match the plan")
|
|
234
|
+
total_downloaded = 0
|
|
235
|
+
for asset in plan.get("assets", []):
|
|
236
|
+
record = records.get(asset["id"])
|
|
237
|
+
if record is None:
|
|
238
|
+
raise ValueError(f"asset record missing: {asset['id']}")
|
|
239
|
+
action = asset["decision"]["action"]
|
|
240
|
+
accepted = {
|
|
241
|
+
"metadata": {"metadata_verified", "prepared"},
|
|
242
|
+
"stream": {"stream_ready", "prepared"},
|
|
243
|
+
"blocked": {"blocked"},
|
|
244
|
+
"inspect": {"prepared", "stream_ready"} if asset.get("supportsStreaming") else {"prepared"},
|
|
245
|
+
"prepare": {"prepared"},
|
|
246
|
+
}[action]
|
|
247
|
+
if record.get("status") not in accepted:
|
|
248
|
+
raise ValueError(f"asset status mismatch: {asset['id']}")
|
|
249
|
+
validate_asset_identity(asset, record)
|
|
250
|
+
if record.get("status") in {"prepared", "stream_ready"}:
|
|
251
|
+
total_downloaded += validate_acquisition(asset, record)
|
|
252
|
+
inventory_asset(asset, record)
|
|
253
|
+
if total_downloaded > plan["limits"]["totalDownloadBytes"]:
|
|
254
|
+
raise ValueError("prepared assets exceed the total download budget")
|
|
255
|
+
total_prepared = sum(
|
|
256
|
+
record.get("totalBytes", 0)
|
|
257
|
+
for record in records.values()
|
|
258
|
+
if isinstance(record.get("totalBytes", 0), int)
|
|
259
|
+
)
|
|
260
|
+
if total_prepared > plan["limits"]["totalExpandedBytes"]:
|
|
261
|
+
raise ValueError("prepared assets exceed the total disk budget")
|
|
262
|
+
disk = os.statvfs(JOB_ROOT)
|
|
263
|
+
free_bytes = disk.f_bavail * disk.f_frsize
|
|
264
|
+
if free_bytes < plan["limits"]["minimumFreeBytesAfterPreparation"]:
|
|
265
|
+
raise ValueError("free disk space after preparation is below the approved reserve")
|
|
266
|
+
checks = {item.get("checkId"): item for item in manifest.get("checks", []) if isinstance(item, dict)}
|
|
267
|
+
if len(checks) != len(manifest.get("checks", [])):
|
|
268
|
+
raise ValueError("asset manifest contains duplicate or invalid checks")
|
|
269
|
+
if set(checks) != {check["id"] for check in plan.get("checks", [])}:
|
|
270
|
+
raise ValueError("asset manifest checks do not exactly match the plan")
|
|
271
|
+
for check in plan.get("checks", []):
|
|
272
|
+
record = checks.get(check["id"])
|
|
273
|
+
if record is None or record.get("kind") != check["kind"]:
|
|
274
|
+
raise ValueError(f"preflight record missing: {check['id']}")
|
|
275
|
+
validate_evidence(check, record)
|
|
276
|
+
manifest["runtimeEnvironment"] = runtime_environment()
|
|
277
|
+
manifest["validatedAt"] = __import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat().replace("+00:00", "Z")
|
|
278
|
+
manifest.pop("manifestDigest", None)
|
|
279
|
+
manifest["manifestDigest"] = digest_bytes(canonical(manifest))
|
|
280
|
+
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
281
|
+
print(json.dumps({"status": "passed", "manifestDigest": manifest["manifestDigest"]}))
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
try:
|
|
287
|
+
raise SystemExit(main())
|
|
288
|
+
except Exception as error:
|
|
289
|
+
print(f"asset manifest validation failed: {error}", file=sys.stderr)
|
|
290
|
+
raise SystemExit(2)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// No model service or paper execution: exercise Docker mounts, Python isolation
|
|
2
|
+
// and the authenticated host relay with a local HTTP fixture.
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import http from 'node:http';
|
|
5
|
+
import { mkdtemp, mkdir, readFile, rm } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { startProviderSession } from '../src/provider/relay.mjs';
|
|
9
|
+
import { buildDockerRun } from '../src/sandbox/docker.mjs';
|
|
10
|
+
import { runProcess } from '../src/process.mjs';
|
|
11
|
+
import { HANDOFF_SCHEMA, stageExecutionHandoff } from '../src/deployment/handoff.mjs';
|
|
12
|
+
|
|
13
|
+
const root=await mkdtemp(path.join(tmpdir(),'CiteArk 本地 runtime '));
|
|
14
|
+
const requests=[];
|
|
15
|
+
const api=http.createServer((request,response)=>{
|
|
16
|
+
requests.push({url:request.url,authorization:request.headers.authorization});
|
|
17
|
+
request.resume();
|
|
18
|
+
response.writeHead(200,{'content-type':'application/json'});
|
|
19
|
+
response.end(JSON.stringify({choices:[{message:{role:'assistant',content:'local-fixture-ok'}}]}));
|
|
20
|
+
});
|
|
21
|
+
let relay;
|
|
22
|
+
try {
|
|
23
|
+
await new Promise(resolve=>api.listen(0,'127.0.0.1',resolve));
|
|
24
|
+
const runtimeAgent={runtime:'opencode',model:'fixture',api:{protocol:'openai-chat-completions',
|
|
25
|
+
baseUrl:`http://127.0.0.1:${api.address().port}`,apiKey:'local-fixture-credential',credentialSource:'test'}};
|
|
26
|
+
relay=await startProviderSession(runtimeAgent);
|
|
27
|
+
const directories=Object.fromEntries(['input','workspace','output','runtimeHome','execution'].map(k=>[k,path.join(root,k)]));
|
|
28
|
+
await Promise.all(Object.values(directories).map(p=>mkdir(p,{recursive:true})));
|
|
29
|
+
await mkdir(path.join(directories.workspace,'repository'));
|
|
30
|
+
const bundle={runId:`local-probe-${Date.now()}`,directories,runtimeAgent,
|
|
31
|
+
executionEnvironment:{image:'citeark-agent/runtime:cpu',cpus:1,memoryGb:2,shmGb:1,gpu:'none'}};
|
|
32
|
+
await stageExecutionHandoff(bundle,{schema:HANDOFF_SCHEMA,instructions:'Local runtime fixture only.',files:[]});
|
|
33
|
+
const python=`import json,os,pathlib,urllib.request,torch
|
|
34
|
+
cfg=json.loads(os.environ['OPENCODE_CONFIG_CONTENT'])
|
|
35
|
+
url=cfg['provider']['citeark']['options']['baseURL']+'/chat/completions'
|
|
36
|
+
req=urllib.request.Request(url,data=json.dumps({'model':'fixture','messages':[{'role':'user','content':'fixture'}]}).encode(),headers={'Content-Type':'application/json','Authorization':'Bearer '+os.environ['CITEARK_API_KEY']})
|
|
37
|
+
with urllib.request.urlopen(req,timeout=30) as r: answer=json.load(r)
|
|
38
|
+
assert answer['choices'][0]['message']['content']=='local-fixture-ok'
|
|
39
|
+
assert (torch.ones((8,8)) @ torch.ones((8,8))).sum().item()==512
|
|
40
|
+
pathlib.Path('/job/workspace/repository/源文件.txt').write_text('workspace-ok')
|
|
41
|
+
pathlib.Path('/job/assets/input.txt').write_text('external-input-fixture')
|
|
42
|
+
pathlib.Path('/job/output/probe.json').write_text(json.dumps({'relay':'ok','python':os.environ['VIRTUAL_ENV'],'torch':torch.__version__}))`;
|
|
43
|
+
const invocation=buildDockerRun({bundle,runtimeCommand:'python',runtimeArgs:['-c',python],attempt:1,providerSession:relay});
|
|
44
|
+
const result=await runProcess(invocation.command,invocation.args,{timeoutMs:120000});
|
|
45
|
+
assert.equal(result.code,0);
|
|
46
|
+
const output=JSON.parse(await readFile(path.join(directories.output,'probe.json'),'utf8'));
|
|
47
|
+
assert.equal(output.python,'/job/runtime-venv');
|
|
48
|
+
assert.equal(await readFile(path.join(directories.workspace,'repository','源文件.txt'),'utf8'),'workspace-ok');
|
|
49
|
+
assert.equal(requests.length,1);
|
|
50
|
+
assert.equal(requests[0].authorization,'Bearer local-fixture-credential');
|
|
51
|
+
console.log(JSON.stringify({status:'passed',mounts:'unicode-and-space-paths',relay:'container-to-host',...output}));
|
|
52
|
+
} finally {
|
|
53
|
+
await relay?.close();
|
|
54
|
+
api.closeAllConnections();
|
|
55
|
+
await new Promise(resolve=>api.close(resolve));
|
|
56
|
+
await rm(root,{recursive:true,force:true});
|
|
57
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { access, mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
|
|
9
|
+
const run = promisify(execFile);
|
|
10
|
+
const root = fileURLToPath(new URL('..', import.meta.url));
|
|
11
|
+
const manifest = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
12
|
+
const temporary = await mkdtemp(path.join(tmpdir(), 'citeark-npm-'));
|
|
13
|
+
if (!process.env.npm_execpath) throw Error('请运行 npm run test:package。');
|
|
14
|
+
const npm = (args, options = {}) => run(process.execPath, [process.env.npm_execpath, ...args], { cwd: root, ...options });
|
|
15
|
+
try {
|
|
16
|
+
const { stdout } = await npm(['pack', '--json', '--ignore-scripts', '--pack-destination', temporary]);
|
|
17
|
+
const [packed] = JSON.parse(stdout);
|
|
18
|
+
const files = new Set(packed.files.map(file => file.path));
|
|
19
|
+
for (const required of ['src/cli.mjs', 'src/public/host.mjs', 'src/public/cap.mjs',
|
|
20
|
+
'dist/arkgraph/viewer.js', 'dist/arkgraph/viewer.css', 'dist/arkgraph/viewer.en.js', 'dist/arkgraph/viewer.en.css', 'dist/arkgraph/index.html', 'dist/arkgraph/boot.js', 'scripts/preview-arkgraph.mjs',
|
|
21
|
+
'docker/claude-code/Dockerfile', 'runtime/requirements-baseline.txt', 'runtime/install-local-cpu-runtime.sh',
|
|
22
|
+
'scripts/run-research-plan.mjs', 'examples/toy-evaluation/paper.md', 'protocol/CAP.md',
|
|
23
|
+
'runtime/mineru/parse.py', 'runtime/mineru/requirements.txt', 'src/research/paper-command.mjs']) {
|
|
24
|
+
assert.ok(files.has(required), `Missing runtime input: ${required}`);
|
|
25
|
+
}
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
if (file.startsWith('dist/arkgraph/')) continue;
|
|
28
|
+
assert.doesNotMatch(file, /(^|\/)(?:\.env(?:\.|$)|\.npmrc$|\.git\/|node_modules\/|tests\/|dist\/)/);
|
|
29
|
+
}
|
|
30
|
+
const prefix = path.join(temporary, 'install');
|
|
31
|
+
await npm(['install', '--global', '--prefix', prefix, '--no-audit', '--no-fund', path.join(temporary, packed.filename)]);
|
|
32
|
+
const windows = process.platform === 'win32';
|
|
33
|
+
const bin = name => path.join(prefix, windows ? `${name}.cmd` : `bin/${name}`);
|
|
34
|
+
const cli = (name, args) => run(windows ? `"${bin(name)}"` : bin(name), args, { cwd: temporary, shell: windows });
|
|
35
|
+
for (const name of ['citeark', 'citeark-agent']) {
|
|
36
|
+
assert.equal((await cli(name, ['--version'])).stdout.trim(), manifest.version);
|
|
37
|
+
}
|
|
38
|
+
assert.match((await cli('citeark', ['--help'])).stdout, /citeark start/);
|
|
39
|
+
assert.match((await cli('citeark', ['--help'])).stdout, /citeark paper setup/);
|
|
40
|
+
// No terminal means help, not a blocking prompt or an accidental research run.
|
|
41
|
+
assert.match((await cli('citeark', [])).stdout, /CiteArk Agent/);
|
|
42
|
+
await cli('citeark', ['init', '--dir', 'example']);
|
|
43
|
+
await access(path.join(temporary, 'example', 'repository', 'evaluate.py'));
|
|
44
|
+
await cli('citeark', ['start', '--paper', 'example/paper.md', '--dry-run', '--work-dir', 'research']);
|
|
45
|
+
const session = JSON.parse(await readFile(path.join(temporary, 'research', 'workspace.json'), 'utf8'));
|
|
46
|
+
assert.equal(session.input.platformRepositoryId, null);
|
|
47
|
+
// Resolve the exported APIs from a consumer next to the installed node_modules.
|
|
48
|
+
const consumerRoot = windows ? prefix : path.join(prefix, 'lib');
|
|
49
|
+
await run(process.execPath, ['--input-type=module', '-e',
|
|
50
|
+
`import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url';
|
|
51
|
+
const require = createRequire(${JSON.stringify(path.join(consumerRoot, 'consumer.cjs'))});
|
|
52
|
+
await import(pathToFileURL(require.resolve('@citeark/agent/host')));
|
|
53
|
+
await import(pathToFileURL(require.resolve('@citeark/agent/cap')));`], { cwd: temporary });
|
|
54
|
+
console.log(`${manifest.name}@${manifest.version}: global commands, help, example, independent dry-run and public APIs passed (${packed.entryCount} package files).`);
|
|
55
|
+
} finally {
|
|
56
|
+
await rm(temporary, { recursive: true, force: true });
|
|
57
|
+
}
|