@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,129 @@
|
|
|
1
|
+
import { AutoDlProClient } from "./client.mjs";
|
|
2
|
+
import { CiteArkError } from "../util.mjs";
|
|
3
|
+
|
|
4
|
+
export const AUTODL_TOOL_DEFINITIONS = Object.freeze([
|
|
5
|
+
tool("autodl_instances_list", "列出 CiteArk 后端可管理的 AutoDL Pro 实例", {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {},
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
}),
|
|
10
|
+
tool("autodl_instance_status", "查询一个 AutoDL Pro 实例的状态", instanceSchema()),
|
|
11
|
+
tool("autodl_instance_start", "启动一个已停止的 AutoDL Pro 实例", instanceSchema()),
|
|
12
|
+
tool("autodl_instance_stop", "关闭一个 AutoDL Pro 实例并保留系统盘", instanceSchema()),
|
|
13
|
+
tool("autodl_instance_release", "永久释放一个已关闭的 AutoDL Pro 实例", {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
instanceUuid: instanceUuidSchema(),
|
|
17
|
+
confirmInstanceUuid: instanceUuidSchema(),
|
|
18
|
+
},
|
|
19
|
+
required: ["instanceUuid", "confirmInstanceUuid"],
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
}),
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function createAutoDlToolExecutor({
|
|
25
|
+
token,
|
|
26
|
+
apiBaseUrl,
|
|
27
|
+
client = new AutoDlProClient({ token, baseUrl: apiBaseUrl }),
|
|
28
|
+
audit = async () => {},
|
|
29
|
+
} = {}) {
|
|
30
|
+
return async function executeAutoDlTool(name, input = {}) {
|
|
31
|
+
const instanceUuid = input?.instanceUuid;
|
|
32
|
+
if (name === "autodl_instances_list") {
|
|
33
|
+
const response = await client.listInstances();
|
|
34
|
+
return {
|
|
35
|
+
instances: (response?.list ?? []).map(publicInstance),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
assertInstanceUuid(instanceUuid);
|
|
39
|
+
if (name === "autodl_instance_status") {
|
|
40
|
+
return { instanceUuid, status: await client.getStatus(instanceUuid) };
|
|
41
|
+
}
|
|
42
|
+
if (name === "autodl_instance_start") {
|
|
43
|
+
const previousStatus = normalizedStatus(await client.getStatus(instanceUuid));
|
|
44
|
+
if (previousStatus !== "running") {
|
|
45
|
+
await client.powerOn(instanceUuid, { startCommand: "sleep infinity" });
|
|
46
|
+
}
|
|
47
|
+
await auditEvent(audit, name, instanceUuid, previousStatus, "power-on-requested");
|
|
48
|
+
return { instanceUuid, previousStatus, action: "power-on-requested" };
|
|
49
|
+
}
|
|
50
|
+
if (name === "autodl_instance_stop") {
|
|
51
|
+
const previousStatus = normalizedStatus(await client.getStatus(instanceUuid));
|
|
52
|
+
if (!stopped(previousStatus)) await client.powerOff(instanceUuid);
|
|
53
|
+
await auditEvent(audit, name, instanceUuid, previousStatus, "power-off-requested");
|
|
54
|
+
return { instanceUuid, previousStatus, action: "power-off-requested" };
|
|
55
|
+
}
|
|
56
|
+
if (name === "autodl_instance_release") {
|
|
57
|
+
if (input.confirmInstanceUuid !== instanceUuid) {
|
|
58
|
+
throw new CiteArkError("释放 AutoDL 实例需要完整 UUID 二次确认");
|
|
59
|
+
}
|
|
60
|
+
const previousStatus = normalizedStatus(await client.getStatus(instanceUuid));
|
|
61
|
+
if (!stopped(previousStatus)) {
|
|
62
|
+
await client.powerOff(instanceUuid);
|
|
63
|
+
await client.waitForStatus(instanceUuid, ["stopped", "shutdown", "shutoff"]);
|
|
64
|
+
}
|
|
65
|
+
await client.release(instanceUuid);
|
|
66
|
+
await auditEvent(audit, name, instanceUuid, previousStatus, "released");
|
|
67
|
+
return { instanceUuid, previousStatus, action: "released" };
|
|
68
|
+
}
|
|
69
|
+
throw new CiteArkError(`未知 AutoDL 工具:${name}`);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function tool(name, description, parameters) {
|
|
74
|
+
return { type: "function", name, description, parameters, strict: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function instanceSchema() {
|
|
78
|
+
return {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: { instanceUuid: instanceUuidSchema() },
|
|
81
|
+
required: ["instanceUuid"],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function instanceUuidSchema() {
|
|
87
|
+
return {
|
|
88
|
+
type: "string",
|
|
89
|
+
pattern: "^pro-[a-zA-Z0-9]+$",
|
|
90
|
+
description: "AutoDL Pro 实例 UUID",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertInstanceUuid(value) {
|
|
95
|
+
if (typeof value !== "string" || !/^pro-[a-zA-Z0-9]+$/.test(value)) {
|
|
96
|
+
throw new CiteArkError("AutoDL 实例 UUID 无效");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizedStatus(value) {
|
|
101
|
+
return String(value ?? "unknown").trim().toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stopped(status) {
|
|
105
|
+
return new Set(["stopped", "shutdown", "shutoff"]).has(status);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function publicInstance(instance) {
|
|
109
|
+
return {
|
|
110
|
+
instanceUuid: instance?.uuid ?? null,
|
|
111
|
+
name: instance?.name ?? null,
|
|
112
|
+
status: instance?.status ?? null,
|
|
113
|
+
region: instance?.region_name ?? instance?.region_sign ?? null,
|
|
114
|
+
gpuSpecUuid: instance?.gpu_spec_uuid ?? null,
|
|
115
|
+
gpuCount: instance?.req_gpu_amount ?? null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function auditEvent(audit, toolName, instanceUuid, previousStatus, action) {
|
|
120
|
+
await audit({
|
|
121
|
+
schemaVersion: "0.1",
|
|
122
|
+
type: "autodl.lifecycle-tool",
|
|
123
|
+
toolName,
|
|
124
|
+
instanceUuid,
|
|
125
|
+
previousStatus,
|
|
126
|
+
action,
|
|
127
|
+
recordedAt: new Date().toISOString(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { CiteArkError, isRecord } from "../util.mjs";
|
|
4
|
+
|
|
5
|
+
const SENSITIVE_KEY = /(api[_-]?key|auth[_-]?token|access[_-]?token|secret|password|credential)/i;
|
|
6
|
+
const SECRET_PATTERNS = [
|
|
7
|
+
/\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
8
|
+
/\b(?:api[_-]?key|auth[_-]?token|access[_-]?token|password)\s*[:=]\s*["']?[^\s"']{12,}/gi,
|
|
9
|
+
/\bAuthorization\s*:\s*Bearer\s+[^\s]+/gi,
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function redactSensitive(value, key = "") {
|
|
13
|
+
if (SENSITIVE_KEY.test(key)) return "[REDACTED]";
|
|
14
|
+
if (typeof value === "string") return redactString(value);
|
|
15
|
+
if (Array.isArray(value)) return value.map((item) => redactSensitive(item));
|
|
16
|
+
if (isRecord(value)) {
|
|
17
|
+
return Object.fromEntries(Object.entries(value).map(([childKey, child]) => [childKey, redactSensitive(child, childKey)]));
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function redactString(input) {
|
|
23
|
+
return SECRET_PATTERNS.reduce((value, pattern) => value.replace(pattern, "[REDACTED]"), input);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function assertEvidenceHasNoLikelySecret(target, label) {
|
|
27
|
+
const stat = await lstat(target);
|
|
28
|
+
if (!stat.isFile() || stat.size > 10 * 1024 * 1024) return;
|
|
29
|
+
const buffer = await readFile(target);
|
|
30
|
+
if (buffer.includes(0)) return;
|
|
31
|
+
const text = buffer.toString("utf8");
|
|
32
|
+
if (SECRET_PATTERNS.some((pattern) => {
|
|
33
|
+
pattern.lastIndex = 0;
|
|
34
|
+
return pattern.test(text);
|
|
35
|
+
})) {
|
|
36
|
+
throw new CiteArkError(`证据疑似包含凭证,CAP 已拒绝发布:${label}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { link, lstat, mkdtemp, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { captureProcess } from "../../process.mjs";
|
|
6
|
+
import { CiteArkError, pathExists, sha256File, sha256Value } from "../../util.mjs";
|
|
7
|
+
import {
|
|
8
|
+
CAP_ARCHIVE_MEDIA_TYPE,
|
|
9
|
+
CAP_MANIFEST,
|
|
10
|
+
safeCapPath,
|
|
11
|
+
} from "./core.mjs";
|
|
12
|
+
import { verifyCapDirectory } from "./verify.mjs";
|
|
13
|
+
|
|
14
|
+
export class CapArchiveConflictError extends CiteArkError {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.failureCategory = "archive";
|
|
18
|
+
this.failureCode = "target.archive_conflict";
|
|
19
|
+
this.retryable = false;
|
|
20
|
+
this.retryStrategy = "none";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function packCap({ directory, outputPath, reuseExisting = false }) {
|
|
25
|
+
const source = path.resolve(directory);
|
|
26
|
+
const destination = path.resolve(outputPath);
|
|
27
|
+
if (path.extname(destination).toLowerCase() !== ".cap") {
|
|
28
|
+
throw new CiteArkError("CAP 2.0 bundles must use the .cap suffix");
|
|
29
|
+
}
|
|
30
|
+
if (destination.startsWith(`${source}${path.sep}`)) {
|
|
31
|
+
throw new CiteArkError("CAP bundle cannot be written inside its source directory");
|
|
32
|
+
}
|
|
33
|
+
const verification = await verifyCapDirectory(source);
|
|
34
|
+
if (!verification.valid) {
|
|
35
|
+
throw Object.assign(new CiteArkError(`CAP 2.0 verification failed before packing:\n- ${verification.issues.join("\n- ")}`), {
|
|
36
|
+
failureCategory: "archive", failureCode: "target.archive_source_invalid", retryable: false, retryStrategy: "none",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const contentDigest = await capDirectoryContentDigest(source);
|
|
40
|
+
const existing = async () => {
|
|
41
|
+
if (!reuseExisting) throw new CapArchiveConflictError(`CAP output already exists: ${destination}`);
|
|
42
|
+
const stat = await lstat(destination);
|
|
43
|
+
const retained = stat.isFile() ? await verifyCapArchive(destination) : null;
|
|
44
|
+
if (!retained?.valid || retained.artifactDigest !== verification.artifactDigest || retained.archive.contentDigest !== contentDigest) {
|
|
45
|
+
throw new CapArchiveConflictError(`Existing CAP does not match the verified artifact: ${destination}`);
|
|
46
|
+
}
|
|
47
|
+
return { ...retained.archive, artifactDigest: retained.artifactDigest };
|
|
48
|
+
};
|
|
49
|
+
if (await pathExists(destination)) return existing();
|
|
50
|
+
// Publish only a complete file, with exclusive creation. A process failure
|
|
51
|
+
// before the stage checkpoint may leave a valid archive, which a resume can
|
|
52
|
+
// verify and reuse without rewriting its signed bytes.
|
|
53
|
+
const temporary = await mkdtemp(path.join(path.dirname(destination), ".cap-building-"));
|
|
54
|
+
try {
|
|
55
|
+
const candidate = path.join(temporary, "artifact.cap");
|
|
56
|
+
const result = await captureProcess(
|
|
57
|
+
"tar", ["-czf", candidate, "--exclude=.DS_Store", "--exclude=._*", "."],
|
|
58
|
+
{ cwd: source, env: { ...process.env, COPYFILE_DISABLE: "1" } },
|
|
59
|
+
);
|
|
60
|
+
if (result.code !== 0) throw new CiteArkError(`Cannot create CAP bundle:\n${result.stderr.trim()}`);
|
|
61
|
+
try { await link(candidate, destination); }
|
|
62
|
+
catch (error) { if (error.code === "EEXIST") return await existing(); throw error; }
|
|
63
|
+
const stat = await lstat(destination);
|
|
64
|
+
return {
|
|
65
|
+
protocolVersion: verification.protocolVersion, format: "tar+gzip", mediaType: CAP_ARCHIVE_MEDIA_TYPE,
|
|
66
|
+
path: destination, size: stat.size, contentDigest, sha256: await sha256File(destination), artifactDigest: verification.artifactDigest,
|
|
67
|
+
};
|
|
68
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function verifyCapArchive(archivePath, options = {}) {
|
|
72
|
+
const source = path.resolve(archivePath);
|
|
73
|
+
const issues = [];
|
|
74
|
+
if (path.extname(source).toLowerCase() !== ".cap") issues.push("CAP 2.0 bundle suffix must be .cap");
|
|
75
|
+
const temporary = await mkdtemp(path.join(os.tmpdir(), "citeark-cap-v2-"));
|
|
76
|
+
try {
|
|
77
|
+
const listing = await captureProcess("tar", ["-tzf", source]);
|
|
78
|
+
if (listing.code !== 0) return { valid: false, issues: ["File is not a valid tar+gzip CAP bundle"] };
|
|
79
|
+
const verbose = await captureProcess("tar", ["-tvzf", source]);
|
|
80
|
+
if (verbose.code !== 0 || verbose.stdout.split(/\r?\n/).filter(Boolean).some((line) => !new Set(["-", "d"]).has(line[0]))) {
|
|
81
|
+
return { valid: false, issues: ["CAP bundle contains links or non-regular entry types"] };
|
|
82
|
+
}
|
|
83
|
+
const paths = new Set();
|
|
84
|
+
for (const entry of listing.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
85
|
+
const normalized = normalizeArchiveEntry(entry);
|
|
86
|
+
if (!normalized) continue;
|
|
87
|
+
if (!safeCapPath(normalized) || paths.has(normalized)) issues.push(`Unsafe or duplicate archive path: ${entry}`);
|
|
88
|
+
paths.add(normalized);
|
|
89
|
+
}
|
|
90
|
+
if (!paths.has(CAP_MANIFEST)) issues.push(`CAP 2.0 bundle is missing ${CAP_MANIFEST}`);
|
|
91
|
+
if (issues.length) return { valid: false, issues };
|
|
92
|
+
const extraction = await captureProcess("tar", ["-xzf", source, "-C", temporary]);
|
|
93
|
+
if (extraction.code !== 0) return { valid: false, issues: ["Cannot safely extract CAP bundle"] };
|
|
94
|
+
if (!(await onlyRegularFilesAndDirectories(temporary))) {
|
|
95
|
+
return { valid: false, issues: ["CAP bundle contains links or non-regular entry types"] };
|
|
96
|
+
}
|
|
97
|
+
const verification = await verifyCapDirectory(temporary, options);
|
|
98
|
+
const stat = await lstat(source);
|
|
99
|
+
return {
|
|
100
|
+
...verification,
|
|
101
|
+
valid: verification.valid && issues.length === 0,
|
|
102
|
+
issues: [...issues, ...verification.issues],
|
|
103
|
+
archive: {
|
|
104
|
+
protocolVersion: verification.protocolVersion,
|
|
105
|
+
format: "tar+gzip",
|
|
106
|
+
mediaType: CAP_ARCHIVE_MEDIA_TYPE,
|
|
107
|
+
path: source,
|
|
108
|
+
size: stat.size,
|
|
109
|
+
sha256: await sha256File(source),
|
|
110
|
+
contentDigest: await capDirectoryContentDigest(temporary),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
} finally {
|
|
114
|
+
await rm(temporary, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeArchiveEntry(value) {
|
|
119
|
+
return value.replace(/^\.\//, "").replace(/\/$/, "");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function onlyRegularFilesAndDirectories(root) {
|
|
123
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
124
|
+
const target = path.join(root, entry.name);
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
if (!(await onlyRegularFilesAndDirectories(target))) return false;
|
|
127
|
+
} else if (!entry.isFile()) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Binds all archived bytes, including signatures and derived attachments;
|
|
135
|
+
* the manifest Artifact Digest alone does not bind a processing-job signature.
|
|
136
|
+
*/
|
|
137
|
+
export async function capDirectoryContentDigest(directory) {
|
|
138
|
+
const entries = [];
|
|
139
|
+
async function scan(current, prefix = "") {
|
|
140
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
141
|
+
if (entry.name === ".DS_Store" || entry.name.startsWith("._")) continue;
|
|
142
|
+
const relative = prefix + entry.name;
|
|
143
|
+
const filename = path.join(current, entry.name);
|
|
144
|
+
if (entry.isDirectory()) await scan(filename, `${relative}/`);
|
|
145
|
+
else if (entry.isFile()) entries.push([relative, await sha256File(filename)]);
|
|
146
|
+
else throw new CapArchiveConflictError(`CAP contains a non-regular entry: ${relative}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await scan(directory);
|
|
150
|
+
entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
151
|
+
return `sha256:${sha256Value(entries)}`;
|
|
152
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPrivateKey,
|
|
3
|
+
createPublicKey,
|
|
4
|
+
generateKeyPairSync,
|
|
5
|
+
sign,
|
|
6
|
+
verify,
|
|
7
|
+
} from "node:crypto";
|
|
8
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
assertSha256Digest,
|
|
13
|
+
canonicalJsonBytes,
|
|
14
|
+
parseCanonicalJson,
|
|
15
|
+
sha256Bytes,
|
|
16
|
+
} from "./canonical-json.mjs";
|
|
17
|
+
|
|
18
|
+
export const CAP_ATTESTATION_MEDIA_TYPE = "application/vnd.citeark.cap.attestation.v2+json";
|
|
19
|
+
export const CAP_PREDICATE_TYPE = "https://citeark.com/cap/attestations/artifact/v2";
|
|
20
|
+
export const IN_TOTO_STATEMENT_V1 = "https://in-toto.io/Statement/v1";
|
|
21
|
+
export const IN_TOTO_PAYLOAD_TYPE = "application/vnd.in-toto+json";
|
|
22
|
+
|
|
23
|
+
export async function capPublicKeyDigestFromPrivateKeyFile(signingKeyPath) {
|
|
24
|
+
const privateKey = createPrivateKey(await readFile(signingKeyPath, "utf8"));
|
|
25
|
+
return capPublicKeyIdentity(privateKey).publicKeyDigest;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function signCapArtifact({
|
|
29
|
+
artifactDigest,
|
|
30
|
+
actor,
|
|
31
|
+
role = "artifactAssembler",
|
|
32
|
+
createdAt = new Date().toISOString(),
|
|
33
|
+
signingKeyPath,
|
|
34
|
+
processingBinding,
|
|
35
|
+
trust = signingKeyPath ? "configured-key" : "local-ephemeral",
|
|
36
|
+
}) {
|
|
37
|
+
assertSha256Digest(artifactDigest, "artifactDigest");
|
|
38
|
+
const privateKey = signingKeyPath
|
|
39
|
+
? createPrivateKey(await readFile(signingKeyPath, "utf8"))
|
|
40
|
+
: generateKeyPairSync("ed25519").privateKey;
|
|
41
|
+
const { publicKeyPem, publicKeyDigest } = capPublicKeyIdentity(privateKey);
|
|
42
|
+
const statement = createCapStatement({
|
|
43
|
+
artifactDigest,
|
|
44
|
+
actor,
|
|
45
|
+
role,
|
|
46
|
+
createdAt,
|
|
47
|
+
processingBinding,
|
|
48
|
+
});
|
|
49
|
+
const payload = canonicalJsonBytes(statement);
|
|
50
|
+
const signature = sign(null, dssePreAuthenticationEncoding(IN_TOTO_PAYLOAD_TYPE, payload), privateKey);
|
|
51
|
+
return {
|
|
52
|
+
mediaType: CAP_ATTESTATION_MEDIA_TYPE,
|
|
53
|
+
envelope: {
|
|
54
|
+
payloadType: IN_TOTO_PAYLOAD_TYPE,
|
|
55
|
+
payload: payload.toString("base64"),
|
|
56
|
+
signatures: [{
|
|
57
|
+
keyid: publicKeyDigest,
|
|
58
|
+
sig: signature.toString("base64"),
|
|
59
|
+
}],
|
|
60
|
+
},
|
|
61
|
+
verificationMaterial: {
|
|
62
|
+
kind: "public-key",
|
|
63
|
+
keyId: publicKeyDigest,
|
|
64
|
+
publicKeyPem,
|
|
65
|
+
trust,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function capPublicKeyIdentity(privateKey) {
|
|
71
|
+
const publicKey = createPublicKey(privateKey);
|
|
72
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
73
|
+
return {
|
|
74
|
+
publicKeyPem,
|
|
75
|
+
publicKeyDigest: sha256Bytes(Buffer.from(publicKeyPem, "utf8")),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createCapStatement({
|
|
80
|
+
artifactDigest,
|
|
81
|
+
actor,
|
|
82
|
+
role,
|
|
83
|
+
createdAt,
|
|
84
|
+
processingBinding,
|
|
85
|
+
}) {
|
|
86
|
+
const digest = assertSha256Digest(artifactDigest, "artifactDigest");
|
|
87
|
+
return {
|
|
88
|
+
_type: IN_TOTO_STATEMENT_V1,
|
|
89
|
+
subject: [{
|
|
90
|
+
name: "CiteArk CAP Artifact",
|
|
91
|
+
digest: { sha256: digest.slice(7) },
|
|
92
|
+
}],
|
|
93
|
+
predicateType: CAP_PREDICATE_TYPE,
|
|
94
|
+
predicate: {
|
|
95
|
+
artifactDigest: digest,
|
|
96
|
+
attestationRole: role,
|
|
97
|
+
actor,
|
|
98
|
+
createdAt,
|
|
99
|
+
...(processingBinding ? { processingBinding } : {}),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function attachCapAttestation({ directory, attestation }) {
|
|
105
|
+
const bytes = canonicalJsonBytes(attestation);
|
|
106
|
+
const digest = sha256Bytes(bytes);
|
|
107
|
+
const targetDirectory = path.join(directory, "attestations");
|
|
108
|
+
const target = path.join(targetDirectory, `${digest.slice(7)}.dsse.json`);
|
|
109
|
+
await mkdir(targetDirectory, { recursive: true });
|
|
110
|
+
await writeFile(target, bytes);
|
|
111
|
+
return { path: `attestations/${digest.slice(7)}.dsse.json`, digest, size: bytes.byteLength };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function verifyCapAttestation(bundle, {
|
|
115
|
+
artifactDigest,
|
|
116
|
+
trustedKeyDigests = [],
|
|
117
|
+
} = {}) {
|
|
118
|
+
const issues = [];
|
|
119
|
+
const warnings = [];
|
|
120
|
+
if (!bundle || bundle.mediaType !== CAP_ATTESTATION_MEDIA_TYPE) {
|
|
121
|
+
return { recognized: false, signatureValid: false, subjectValid: false, trusted: false, issues, warnings };
|
|
122
|
+
}
|
|
123
|
+
const envelope = bundle.envelope;
|
|
124
|
+
const material = bundle.verificationMaterial;
|
|
125
|
+
if (envelope?.payloadType !== IN_TOTO_PAYLOAD_TYPE || typeof envelope.payload !== "string") {
|
|
126
|
+
issues.push("DSSE envelope has an invalid payload type or payload");
|
|
127
|
+
return result();
|
|
128
|
+
}
|
|
129
|
+
const signatures = Array.isArray(envelope.signatures) ? envelope.signatures : [];
|
|
130
|
+
if (signatures.length === 0) issues.push("DSSE envelope contains no signatures");
|
|
131
|
+
if (material?.kind !== "public-key" || typeof material.publicKeyPem !== "string") {
|
|
132
|
+
issues.push("Reference verifier requires embedded public-key verification material");
|
|
133
|
+
return result();
|
|
134
|
+
}
|
|
135
|
+
const publicKeyDigest = sha256Bytes(Buffer.from(material.publicKeyPem, "utf8"));
|
|
136
|
+
if (material.keyId !== publicKeyDigest) issues.push("Attestation public key id does not match its bytes");
|
|
137
|
+
let statement;
|
|
138
|
+
let payload;
|
|
139
|
+
try {
|
|
140
|
+
payload = Buffer.from(envelope.payload, "base64");
|
|
141
|
+
statement = parseCanonicalJson(payload, "in-toto Statement");
|
|
142
|
+
} catch (error) {
|
|
143
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
144
|
+
return result();
|
|
145
|
+
}
|
|
146
|
+
const matchingSignatures = signatures.filter((item) => item?.keyid === publicKeyDigest && typeof item.sig === "string");
|
|
147
|
+
let signatureValid = false;
|
|
148
|
+
try {
|
|
149
|
+
const publicKey = createPublicKey(material.publicKeyPem);
|
|
150
|
+
signatureValid = matchingSignatures.some((item) => verify(
|
|
151
|
+
null,
|
|
152
|
+
dssePreAuthenticationEncoding(envelope.payloadType, payload),
|
|
153
|
+
publicKey,
|
|
154
|
+
Buffer.from(item.sig, "base64"),
|
|
155
|
+
));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
issues.push(`Attestation public key or signature is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
|
+
}
|
|
159
|
+
if (!signatureValid) issues.push("No DSSE signature is valid for the embedded public key");
|
|
160
|
+
|
|
161
|
+
const expected = artifactDigest ? assertSha256Digest(artifactDigest, "artifactDigest") : null;
|
|
162
|
+
const subjectDigest = statement?.subject?.find((item) => item?.name === "CiteArk CAP Artifact")?.digest?.sha256;
|
|
163
|
+
const predicateDigest = statement?.predicate?.artifactDigest;
|
|
164
|
+
const subjectValid = statement?._type === IN_TOTO_STATEMENT_V1
|
|
165
|
+
&& statement?.predicateType === CAP_PREDICATE_TYPE
|
|
166
|
+
&& (!expected || subjectDigest === expected.slice(7))
|
|
167
|
+
&& (!expected || predicateDigest === expected);
|
|
168
|
+
if (!subjectValid) issues.push("in-toto Statement does not bind the expected CAP Artifact digest");
|
|
169
|
+
const trusted = trustedKeyDigests.includes(publicKeyDigest);
|
|
170
|
+
if (!trusted) warnings.push(`Signature key is not trusted by the active policy: ${publicKeyDigest}`);
|
|
171
|
+
return {
|
|
172
|
+
recognized: true,
|
|
173
|
+
signatureValid,
|
|
174
|
+
subjectValid,
|
|
175
|
+
trusted,
|
|
176
|
+
publicKeyDigest,
|
|
177
|
+
statement,
|
|
178
|
+
trustLabel: material.trust ?? null,
|
|
179
|
+
issues,
|
|
180
|
+
warnings,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
function result() {
|
|
184
|
+
return {
|
|
185
|
+
recognized: true,
|
|
186
|
+
signatureValid: false,
|
|
187
|
+
subjectValid: false,
|
|
188
|
+
trusted: false,
|
|
189
|
+
issues,
|
|
190
|
+
warnings,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function dssePreAuthenticationEncoding(payloadType, payload) {
|
|
196
|
+
const typeBytes = Buffer.from(payloadType, "utf8");
|
|
197
|
+
const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
198
|
+
return Buffer.concat([
|
|
199
|
+
Buffer.from(`DSSEv1 ${typeBytes.byteLength} `, "utf8"),
|
|
200
|
+
typeBytes,
|
|
201
|
+
Buffer.from(` ${body.byteLength} `, "utf8"),
|
|
202
|
+
body,
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
4
|
+
|
|
5
|
+
export function canonicalJson(value) {
|
|
6
|
+
assertIJson(value);
|
|
7
|
+
return JSON.stringify(sortValue(value));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function canonicalJsonBytes(value) {
|
|
11
|
+
return Buffer.from(canonicalJson(value), "utf8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseCanonicalJson(bytes, label = "JSON document") {
|
|
15
|
+
const source = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
16
|
+
let text;
|
|
17
|
+
try {
|
|
18
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(source);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new Error(`${label} is not valid UTF-8`, { cause: error });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let value;
|
|
24
|
+
try {
|
|
25
|
+
value = JSON.parse(text);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(`${label} is not valid JSON`, { cause: error });
|
|
28
|
+
}
|
|
29
|
+
const canonical = canonicalJson(value);
|
|
30
|
+
if (text !== canonical) {
|
|
31
|
+
throw new Error(`${label} is not encoded as RFC 8785 canonical JSON`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sha256Bytes(bytes) {
|
|
37
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function sha256CanonicalJson(value) {
|
|
41
|
+
return sha256Bytes(canonicalJsonBytes(value));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function assertSha256Digest(value, label = "digest") {
|
|
45
|
+
if (!DIGEST_PATTERN.test(value)) {
|
|
46
|
+
throw new Error(`${label} must be a lowercase sha256 digest`);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function assertIJson(value, pointer = "$", ancestors = new Set()) {
|
|
52
|
+
if (value === null || typeof value === "boolean") return;
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
assertUnicodeScalarString(value, pointer);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === "number") {
|
|
58
|
+
if (!Number.isFinite(value)) throw new Error(`${pointer} must be a finite JSON number`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value !== "object") {
|
|
62
|
+
throw new Error(`${pointer} contains a value that is not representable in JSON`);
|
|
63
|
+
}
|
|
64
|
+
if (ancestors.has(value)) throw new Error(`${pointer} contains a cyclic reference`);
|
|
65
|
+
ancestors.add(value);
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
value.forEach((item, index) => assertIJson(item, `${pointer}/${index}`, ancestors));
|
|
68
|
+
} else {
|
|
69
|
+
const prototype = Object.getPrototypeOf(value);
|
|
70
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
71
|
+
throw new Error(`${pointer} must be a plain JSON object`);
|
|
72
|
+
}
|
|
73
|
+
for (const [key, item] of Object.entries(value)) {
|
|
74
|
+
assertUnicodeScalarString(key, `${pointer} object key`);
|
|
75
|
+
assertIJson(item, `${pointer}/${escapePointer(key)}`, ancestors);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
ancestors.delete(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sortValue(value) {
|
|
82
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
83
|
+
if (value === null || typeof value !== "object") return value;
|
|
84
|
+
return Object.fromEntries(
|
|
85
|
+
Object.keys(value)
|
|
86
|
+
.sort(compareUtf16)
|
|
87
|
+
.map((key) => [key, sortValue(value[key])]),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function compareUtf16(left, right) {
|
|
92
|
+
if (left < right) return -1;
|
|
93
|
+
if (left > right) return 1;
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function assertUnicodeScalarString(value, pointer) {
|
|
98
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
99
|
+
const code = value.charCodeAt(index);
|
|
100
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
101
|
+
const next = value.charCodeAt(index + 1);
|
|
102
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) {
|
|
103
|
+
throw new Error(`${pointer} contains an unpaired high surrogate`);
|
|
104
|
+
}
|
|
105
|
+
index += 1;
|
|
106
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
107
|
+
throw new Error(`${pointer} contains an unpaired low surrogate`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function escapePointer(value) {
|
|
113
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
114
|
+
}
|