blun-king-cli 9.1.566 → 9.1.569
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/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
- package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
- package/agent-spine-plugin/CHANGELOG.md +1581 -0
- package/agent-spine-plugin/README.md +30 -4
- package/agent-spine-plugin/blun.plugin.json +3 -3
- package/agent-spine-plugin/docs/acceptance.md +61 -0
- package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
- package/agent-spine-plugin/docs/host-integration.md +178 -0
- package/agent-spine-plugin/docs/preflight-recall.md +69 -0
- package/agent-spine-plugin/docs/preservation-contract.md +53 -0
- package/agent-spine-plugin/docs/quality-gates.md +50 -0
- package/agent-spine-plugin/docs/releasing.md +85 -0
- package/agent-spine-plugin/docs/session-timeline.md +251 -0
- package/agent-spine-plugin/docs/source-roots.md +113 -0
- package/agent-spine-plugin/docs/structured-completion.md +67 -0
- package/agent-spine-plugin/docs/world-model.md +94 -0
- package/agent-spine-plugin/hooks/codex.json +2 -2
- package/agent-spine-plugin/hooks/hooks.json +1 -1
- package/agent-spine-plugin/hooks/version.json +1 -1
- package/agent-spine-plugin/package.json +13 -3
- package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
- package/agent-spine-plugin/scripts/check-hosts.js +14 -5
- package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
- package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
- package/agent-spine-plugin/scripts/check-install.js +478 -0
- package/agent-spine-plugin/scripts/check-line-budget.js +58 -0
- package/agent-spine-plugin/scripts/check-syntax.js +29 -0
- package/agent-spine-plugin/scripts/github-actions.js +11 -0
- package/agent-spine-plugin/scripts/hermetic-process.js +183 -0
- package/agent-spine-plugin/scripts/release-check.js +145 -0
- package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
- package/agent-spine-plugin/scripts/run-checks.js +47 -0
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
- package/agent-spine-plugin/spine-example/1-identity.md +12 -0
- package/agent-spine-plugin/spine-example/2-voice.md +6 -0
- package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
- package/agent-spine-plugin/spine-example/4-history.md +4 -0
- package/agent-spine-plugin/src/cli-agent.js +296 -0
- package/agent-spine-plugin/src/cli-attention.js +95 -0
- package/agent-spine-plugin/src/cli-autonomy.js +36 -0
- package/agent-spine-plugin/src/cli-common.js +71 -0
- package/agent-spine-plugin/src/cli-continuity.js +116 -0
- package/agent-spine-plugin/src/cli-core.js +128 -0
- package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
- package/agent-spine-plugin/src/cli-host.js +21 -0
- package/agent-spine-plugin/src/cli-learning.js +305 -0
- package/agent-spine-plugin/src/cli-premortem.js +16 -0
- package/agent-spine-plugin/src/cli-sharing.js +230 -0
- package/agent-spine-plugin/src/cli.js +40 -1350
- package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
- package/agent-spine-plugin/src/hook.js +185 -567
- package/agent-spine-plugin/src/index.js +16 -2
- package/agent-spine-plugin/src/lib/acceptance.js +113 -11
- package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
- package/agent-spine-plugin/src/lib/attention-context.js +167 -0
- package/agent-spine-plugin/src/lib/attention-events.js +113 -0
- package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
- package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
- package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
- package/agent-spine-plugin/src/lib/attention.js +9 -600
- package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
- package/agent-spine-plugin/src/lib/audit.js +56 -6
- package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
- package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
- package/agent-spine-plugin/src/lib/autonomy.js +8 -0
- package/agent-spine-plugin/src/lib/briefing.js +67 -4
- package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
- package/agent-spine-plugin/src/lib/catalog.js +1 -1
- package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
- package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
- package/agent-spine-plugin/src/lib/context.js +3 -1
- package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
- package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
- package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
- package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
- package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
- package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
- package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
- package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
- package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
- package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
- package/agent-spine-plugin/src/lib/documents.js +27 -5
- package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
- package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
- package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
- package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
- package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
- package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
- package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
- package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
- package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
- package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
- package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
- package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
- package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
- package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
- package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
- package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
- package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
- package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
- package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
- package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
- package/agent-spine-plugin/src/lib/hook-context.js +424 -0
- package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
- package/agent-spine-plugin/src/lib/hook-input.js +27 -0
- package/agent-spine-plugin/src/lib/hook-output.js +140 -0
- package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
- package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
- package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
- package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
- package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
- package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
- package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
- package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
- package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
- package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
- package/agent-spine-plugin/src/lib/learning-context.js +130 -0
- package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
- package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
- package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
- package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
- package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
- package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
- package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
- package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
- package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
- package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
- package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
- package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
- package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
- package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
- package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
- package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
- package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
- package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
- package/agent-spine-plugin/src/lib/learning.js +36 -6923
- package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
- package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
- package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
- package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
- package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
- package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
- package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
- package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
- package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
- package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
- package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
- package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
- package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
- package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
- package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
- package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
- package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
- package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
- package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
- package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
- package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
- package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
- package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
- package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
- package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
- package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
- package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
- package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
- package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
- package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
- package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
- package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
- package/agent-spine-plugin/src/lib/source-roots.js +69 -136
- package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
- package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
- package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
- package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
- package/agent-spine-plugin/src/lib/world-model.js +278 -0
- package/agent-spine-plugin/src/mcp.js +20 -161
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +22 -5
- package/bin/agent-resume-snapshot.cjs +2 -2
- package/bin/agentspine-king-goal-inbox.mjs +111 -0
- package/bin/agentspine-king-goal-intake.mjs +106 -0
- package/bin/baseline-skill-performance-policy.cjs +1 -16
- package/bin/core-bootstrap.js +2 -0
- package/bin/curiosity-scout-policy.cjs +5 -1
- package/bin/input-draft-persistence.cjs +2 -2
- package/bin/king-tui-function-contract.json +33 -0
- package/bin/launcher-restart-policy.cjs +150 -0
- package/bin/launcher-runtime.js +56 -44
- package/bin/managed-context-startup-policy.cjs +27 -0
- package/bin/managed-plugin-selection.cjs +116 -0
- package/bin/mistake-relevance-policy.cjs +1 -1
- package/bin/observer-hooks.cjs +14 -0
- package/bin/oversized-context-offload-policy.cjs +86 -0
- package/bin/plugin-bootstrap.js +7 -40
- package/bin/proactive-compaction-policy.cjs +1 -1
- package/bin/provider-model-refresh-deadline.cjs +53 -0
- package/bin/provider-model-refresh-policy.cjs +107 -0
- package/bin/release-artifact-freeze-policy.cjs +30 -0
- package/bin/repeated-user-message-projection.cjs +3 -126
- package/bin/research-page-result.cjs +74 -0
- package/bin/runtime-exit-ledger.cjs +1 -0
- package/bin/session-compaction-policy.cjs +84 -0
- package/bin/skill-listing-performance-policy.cjs +2 -2
- package/bin/standard-tools-bootstrap.js +0 -37
- package/bin/subagent-skill-policy.cjs +3 -1
- package/bin/telegram-approval-relay.cjs +12 -7
- package/bin/telegram-private-conversation-policy.cjs +3 -2
- package/bin/telegram-queue-handoff-policy.cjs +24 -0
- package/bin/thinking-activity-status-policy.cjs +1 -1
- package/bin/thinking-only-guard.cjs +16 -12
- package/bin/tool-call-loop-policy.cjs +0 -2
- package/bin/tool-result-offload-policy.cjs +11 -2
- package/bin/tui-functional-contract.cjs +55 -0
- package/bin/update-notice.js +18 -14
- package/bin/user-message-offload-policy.cjs +1 -1
- package/bin/user-prompt-hook-origin-policy.cjs +34 -0
- package/bin/windows-node-crash-dump.cjs +110 -0
- package/blun.mjs +2517 -1048
- package/codebase-index/codebase_index.py +4 -3
- package/package.json +3 -17
- package/standard-skills/research-evidence/SKILL.md +39 -0
- package/standard-skills/research-evidence/references/evidence-format.md +82 -0
- package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
- package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
- package/standard-skills/web-lesen/SKILL.md +37 -22
- package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
- package/telegram-plugin/DELIVERY.md +36 -0
- package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
- package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
- package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
- package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
- package/telegram-plugin/dist/bridge.mjs +11 -57
- package/telegram-plugin/dist/mcp-server.mjs +33 -4
- package/agent-spine-plugin/skill/SKILL.md +0 -76
- package/bin/mnemo-connect-heartbeat.cjs +0 -204
- package/bin/mnemo-tool-agent-policy.cjs +0 -22
- package/telegram-plugin/bin/telegram-mnemo-capture.cjs +0 -297
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { validateCodexTimelineHeader } from "./session-timeline-codex.js";
|
|
2
|
+
import { validateKingTimelineHeader } from "./session-timeline-king.js";
|
|
3
|
+
import { timelineProtocolFromRuntime, validTimelineHost } from "./session-timeline-provider.js";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { isAbsolute } from "node:path";
|
|
6
|
+
import { canonicalPath } from "./paths.js";
|
|
7
|
+
import { withOwnedFileLock } from "./owned-file-lock.js";
|
|
8
|
+
import {
|
|
9
|
+
privateEnrollmentPaths, readPrivateEnrollmentState, savePrivateEnrollmentState
|
|
10
|
+
} from "./session-timeline-enrollment-storage.js";
|
|
11
|
+
import {
|
|
12
|
+
normalizePrivateTimelineSource, PRIVATE_TIMELINE_PREFIX_BYTES, privateTimelinePrefixDigest
|
|
13
|
+
} from "./session-timeline-enrollment-source.js";
|
|
14
|
+
import {
|
|
15
|
+
hostReceiptId, hostReceiptResult, hostReceiptUnavailable, makeHostTranscriptReceipt, MAX_PENDING_RECEIPTS,
|
|
16
|
+
PRIVATE_TIMELINE_HOST_RECEIPT_ORIGIN, PRIVATE_TIMELINE_HOST_RECEIPT_SCHEMA, purgeExpiredHostReceipts,
|
|
17
|
+
sameHostReceiptSource, sameIssuedHostTranscriptEvent, validHostTranscriptReceipt
|
|
18
|
+
} from "./session-timeline-host-receipt.js";
|
|
19
|
+
import {
|
|
20
|
+
completeTimelineBinding, hasTimelineGroupScope, sameTimelineBinding, sessionTimelineBinding,
|
|
21
|
+
safeTimelineId, TIMELINE_ID_RE, validTimelineBinding
|
|
22
|
+
} from "./session-timeline-contract.js";
|
|
23
|
+
import { sameTimelineTransportDigest, timelineTransportDigest, validTimelineTransportDigest } from "./session-timeline-transport.js";
|
|
24
|
+
import {
|
|
25
|
+
sameSessionTimelineSourceLocation, sourcePath, validSessionTimelineSourceMetadata
|
|
26
|
+
} from "./session-timeline-source.js";
|
|
27
|
+
import { validVerifiedTimelineHostOrigin } from "./session-timeline-host-origin.js";
|
|
28
|
+
import { sessionTimelineRootDigest } from "./session-timeline-root.js";
|
|
29
|
+
|
|
30
|
+
export const PRIVATE_TIMELINE_ENROLLMENT_SCHEMA = "agentspine.session-timeline-private-enrollment/v1";
|
|
31
|
+
export { PRIVATE_TIMELINE_HOST_RECEIPT_ORIGIN, PRIVATE_TIMELINE_HOST_RECEIPT_SCHEMA } from "./session-timeline-host-receipt.js";
|
|
32
|
+
export const LOCAL_TIMELINE_ENROLLMENT_CONFIRMATION = "local-owner-confirmed";
|
|
33
|
+
export const LOCAL_TIMELINE_ENROLLMENT_RECOVERY_CONFIRMATION = "local-owner-confirmed-timeline-enrollment-recovery";
|
|
34
|
+
export const DEFAULT_TIMELINE_ENROLLMENT_TTL_MS = 8 * 60 * 60 * 1000;
|
|
35
|
+
export const MAX_TIMELINE_ENROLLMENT_TTL_MS = 12 * 60 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
const AUTHORITY = "context-only";
|
|
38
|
+
const TIMELINE_VISIBILITY = "private-verified";
|
|
39
|
+
const HEAD_SCHEMA = "agentspine.session-timeline-private-enrollment-head/v1";
|
|
40
|
+
const HEAD_AUTHORITY = "state-integrity-only";
|
|
41
|
+
const MAX_STATE_BYTES = 256 * 1024;
|
|
42
|
+
const MAX_ENROLLMENTS = 16;
|
|
43
|
+
function canonical(value) {
|
|
44
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
45
|
+
if (value && typeof value === "object") {
|
|
46
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
47
|
+
}
|
|
48
|
+
return JSON.stringify(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function digest(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
52
|
+
function validSignature(value) { return /^[a-f0-9]{64}$/.test(value || ""); }
|
|
53
|
+
function validGeneration(value) { return Number.isSafeInteger(value) && value > 0; }
|
|
54
|
+
function validDate(value) { return Number.isFinite(new Date(value).getTime()); }
|
|
55
|
+
function date(value) {
|
|
56
|
+
const parsed = value instanceof Date ? value : new Date(value);
|
|
57
|
+
return Number.isFinite(parsed.getTime()) ? parsed : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runtimeDate(clock) {
|
|
61
|
+
try { return date(typeof clock === "function" ? clock() : new Date()); }
|
|
62
|
+
catch { return null; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function enrollmentTtl(value) {
|
|
66
|
+
return Number.isInteger(value) && value >= 60 * 1000 && value <= MAX_TIMELINE_ENROLLMENT_TTL_MS ? value : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function unavailable(reason) {
|
|
70
|
+
return {
|
|
71
|
+
schema: PRIVATE_TIMELINE_ENROLLMENT_SCHEMA, status: "unavailable", reason,
|
|
72
|
+
binding: null, source: null, sourceDigest: null, expiresAt: null,
|
|
73
|
+
enrollmentDigest: null, timelineVisibility: null, authority: AUTHORITY
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function empty(root) {
|
|
78
|
+
return {
|
|
79
|
+
schema: PRIVATE_TIMELINE_ENROLLMENT_SCHEMA, rootDigest: sessionTimelineRootDigest(root),
|
|
80
|
+
enrollments: [], pendingReceipts: [], generation: 0, previousSignature: null, authority: AUTHORITY
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sourceMetadata(source) {
|
|
85
|
+
return {
|
|
86
|
+
path: source?.path, profileRoot: source?.profileRoot, projectsRoot: source?.projectsRoot,
|
|
87
|
+
pathDigest: source?.pathDigest, identity: source?.identity,
|
|
88
|
+
size: source?.size, mtimeNs: source?.mtimeNs, ctimeNs: source?.ctimeNs
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validSource(source) {
|
|
93
|
+
return validSessionTimelineSourceMetadata(sourceMetadata(source))
|
|
94
|
+
&& Number.isSafeInteger(source.prefixBytes) && source.prefixBytes >= 0 && source.prefixBytes <= PRIVATE_TIMELINE_PREFIX_BYTES
|
|
95
|
+
&& /^[a-f0-9]{64}$/.test(source.prefixDigest || "")
|
|
96
|
+
&& source.commitment === null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sourceDigest(source) {
|
|
100
|
+
return digest(`${source.pathDigest}\0${source.identity}\0${source.profileRoot}\0${source.projectsRoot}\0${source.size}\0${source.mtimeNs}\0${source.ctimeNs}\0${source.prefixDigest}\0lazy-snapshot/v1`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sameSource(left, right) { return sameSessionTimelineSourceLocation(sourceMetadata(left), sourceMetadata(right)); }
|
|
104
|
+
|
|
105
|
+
function sameSourceSnapshot(left, right) {
|
|
106
|
+
const first = sourceMetadata(left);
|
|
107
|
+
const second = sourceMetadata(right);
|
|
108
|
+
return validSessionTimelineSourceMetadata(first) && validSessionTimelineSourceMetadata(second)
|
|
109
|
+
&& ["path", "profileRoot", "projectsRoot", "pathDigest", "identity", "size", "mtimeNs", "ctimeNs"]
|
|
110
|
+
.every((key) => first[key] === second[key]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function enrollmentId(root, binding, source) {
|
|
114
|
+
return `timeline-enrollment:${digest(canonical({ rootDigest: sessionTimelineRootDigest(root), binding, sourceDigest: sourceDigest(source) })).slice(0, 32)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function enrollmentDigest(root, binding, source, expiresAt, transportDigest) {
|
|
118
|
+
return digest(canonical({ rootDigest: sessionTimelineRootDigest(root), binding, sourceDigest: sourceDigest(source), expiresAt,
|
|
119
|
+
transportDigest, confirmation: LOCAL_TIMELINE_ENROLLMENT_CONFIRMATION, timelineVisibility: TIMELINE_VISIBILITY }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validEnrollment(value, root) {
|
|
123
|
+
return value && TIMELINE_ID_RE.test(value.id || "") && validTimelineBinding(value.binding)
|
|
124
|
+
&& completeTimelineBinding(value.binding) && value.binding.groupId === null && validSource(value.source)
|
|
125
|
+
&& validDate(value.confirmedAt) && validDate(value.expiresAt) && value.timelineVisibility === TIMELINE_VISIBILITY
|
|
126
|
+
&& value.confirmation === LOCAL_TIMELINE_ENROLLMENT_CONFIRMATION && value.authority === AUTHORITY
|
|
127
|
+
&& validTimelineTransportDigest(value.transportDigest)
|
|
128
|
+
&& value.id === enrollmentId(root, value.binding, value.source)
|
|
129
|
+
&& value.enrollmentDigest === enrollmentDigest(root, value.binding, value.source, value.expiresAt, value.transportDigest)
|
|
130
|
+
&& value.append === undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function unambiguousEnrollments(enrollments) {
|
|
134
|
+
const keys = new Set();
|
|
135
|
+
for (const record of enrollments) {
|
|
136
|
+
const key = [record.binding.host, record.binding.sessionId, sourceDigest(record.source)].join("\0");
|
|
137
|
+
if (keys.has(key)) return false;
|
|
138
|
+
keys.add(key);
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function validate(state, root) {
|
|
144
|
+
const receipts = state?.pendingReceipts === undefined ? [] : state.pendingReceipts;
|
|
145
|
+
if (!state || state.schema !== PRIVATE_TIMELINE_ENROLLMENT_SCHEMA || state.rootDigest !== sessionTimelineRootDigest(root)
|
|
146
|
+
|| state.authority !== AUTHORITY || !Array.isArray(state.enrollments)
|
|
147
|
+
|| !validGeneration(state.generation) || !validSignature(state.signature)
|
|
148
|
+
|| (state.generation === 1 ? state.previousSignature !== null : !validSignature(state.previousSignature))
|
|
149
|
+
|| state.enrollments.length > MAX_ENROLLMENTS || !state.enrollments.every((item) => validEnrollment(item, root))
|
|
150
|
+
|| !unambiguousEnrollments(state.enrollments) || !Array.isArray(receipts) || receipts.length > MAX_PENDING_RECEIPTS
|
|
151
|
+
|| !receipts.every((item) => validHostTranscriptReceipt(item, root))
|
|
152
|
+
|| new Set(receipts.map((item) => item.id)).size !== receipts.length) {
|
|
153
|
+
throw new Error("private timeline enrollment state is invalid");
|
|
154
|
+
}
|
|
155
|
+
return state;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stateRoot(root) { return { value: root, digest: sessionTimelineRootDigest(root) }; }
|
|
159
|
+
|
|
160
|
+
async function enrollmentPaths(root, options = {}) {
|
|
161
|
+
return privateEnrollmentPaths(root, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function readState(path, headPath, root, assertStable, assertOwned = null) {
|
|
165
|
+
const state = await readPrivateEnrollmentState({
|
|
166
|
+
path, headPath, root: stateRoot(root), assertStable, assertOwned, empty, validate,
|
|
167
|
+
headSchema: HEAD_SCHEMA, headAuthority: HEAD_AUTHORITY, maximumBytes: MAX_STATE_BYTES
|
|
168
|
+
});
|
|
169
|
+
if (!Array.isArray(state.pendingReceipts)) state.pendingReceipts = [];
|
|
170
|
+
return state;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function saveState(state, path, headPath, root, assertOwned, assertStable) {
|
|
174
|
+
await savePrivateEnrollmentState({
|
|
175
|
+
state, path, headPath, root: stateRoot(root), assertOwned, assertStable,
|
|
176
|
+
headSchema: HEAD_SCHEMA, headAuthority: HEAD_AUTHORITY, maximumBytes: MAX_STATE_BYTES
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// The normal path remains read-only. If a crash left one verified state
|
|
181
|
+
// generation ahead of its signed head, retry under the existing owned lock so
|
|
182
|
+
// storage can perform its exact one-step repair; every other failure remains
|
|
183
|
+
// unavailable and yields no enrollment or source data.
|
|
184
|
+
async function loadStateWithRecovery(rootPath, { create = false } = {}) {
|
|
185
|
+
const names = await enrollmentPaths(rootPath, { create });
|
|
186
|
+
try {
|
|
187
|
+
return { names, state: await readState(names.path, names.headPath, rootPath, names.assertStable) };
|
|
188
|
+
} catch {
|
|
189
|
+
const state = await withOwnedFileLock(names.lock, async ({ assertOwned }) => readState(
|
|
190
|
+
names.path, names.headPath, rootPath, names.assertStable, assertOwned
|
|
191
|
+
), { assertPath: names.assertStable });
|
|
192
|
+
return { names, state };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function validHostReceiptInput({ host, sessionId, scope }) {
|
|
197
|
+
const binding = sessionTimelineBinding({ host, sessionId, scope });
|
|
198
|
+
if (!validTimelineHost(host)) return { reason: "unknown-timeline-host" };
|
|
199
|
+
if (scope?.groupId !== null || hasTimelineGroupScope(scope)) return { reason: "group-suppressed" };
|
|
200
|
+
if (!completeTimelineBinding(binding) || binding.groupId !== null) return { reason: "missing-or-unknown-session-scope" };
|
|
201
|
+
return { binding };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function validResolverInput({ host, sessionId }) {
|
|
205
|
+
if (!validTimelineHost(host)) return null;
|
|
206
|
+
return safeTimelineId(sessionId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function canonicalRoot(root) {
|
|
210
|
+
if (typeof root !== "string" || !root) return null;
|
|
211
|
+
try { return await canonicalPath(root); }
|
|
212
|
+
catch { return null; }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// This is called only by a verified provider UserPromptSubmit lifecycle adapter. It
|
|
216
|
+
// records a signed, short-lived observation; the opaque token itself is never
|
|
217
|
+
// returned through hook context or a briefing.
|
|
218
|
+
export async function issueHostTranscriptReceipt({
|
|
219
|
+
root, host, sessionId, scope, transcriptPath, hostHome, event, eventId = null,
|
|
220
|
+
hostOrigin = null, clock = null, environment = process.env
|
|
221
|
+
}) {
|
|
222
|
+
const current = runtimeDate(clock);
|
|
223
|
+
if (!current) return hostReceiptUnavailable("invalid-host-receipt-time");
|
|
224
|
+
if (event !== "UserPromptSubmit") return hostReceiptUnavailable("host-receipt-event-not-supported");
|
|
225
|
+
const rootPath = await canonicalRoot(root);
|
|
226
|
+
if (!rootPath) return hostReceiptUnavailable("unknown-project-root");
|
|
227
|
+
const input = validHostReceiptInput({ host, sessionId, scope });
|
|
228
|
+
if (!input.binding) return hostReceiptUnavailable(input.reason);
|
|
229
|
+
const hookInput = { transcript_path: transcriptPath, event_id: eventId,
|
|
230
|
+
protocol_version: timelineProtocolFromRuntime(host, environment) };
|
|
231
|
+
if (!validVerifiedTimelineHostOrigin({ origin: hostOrigin, root: rootPath, input: hookInput,
|
|
232
|
+
binding: input.binding, hostHome, event })) return hostReceiptUnavailable("host-lifecycle-receipt-required");
|
|
233
|
+
const transportDigest = timelineTransportDigest({ root: rootPath, binding: input.binding, environment });
|
|
234
|
+
if (!transportDigest) return hostReceiptUnavailable("local-transport-capability-required");
|
|
235
|
+
const source = await sourcePath(transcriptPath, hostHome, host);
|
|
236
|
+
if (source.status !== "registered") return hostReceiptUnavailable(source.reason || "transcript-unavailable");
|
|
237
|
+
if (host === "codex" && !await validateCodexTimelineHeader({ source, root: rootPath, sessionId })) {
|
|
238
|
+
return hostReceiptUnavailable("codex-history-format-or-binding-mismatch");
|
|
239
|
+
}
|
|
240
|
+
if (host === "king" && !await validateKingTimelineHeader({ source, sessionId,
|
|
241
|
+
protocolVersion: timelineProtocolFromRuntime(host, environment) })) {
|
|
242
|
+
return hostReceiptUnavailable("king-history-format-version-or-binding-mismatch");
|
|
243
|
+
}
|
|
244
|
+
// This bounded 4 KiB digest is an immutable source binding, not transcript
|
|
245
|
+
// capture: receipt issuance never retains or injects the source text.
|
|
246
|
+
const snapshot = await normalizePrivateTimelineSource(source);
|
|
247
|
+
if (!snapshot) return hostReceiptUnavailable("transcript-snapshot-unavailable");
|
|
248
|
+
try {
|
|
249
|
+
const names = await enrollmentPaths(rootPath);
|
|
250
|
+
return await withOwnedFileLock(names.lock, async ({ assertOwned }) => {
|
|
251
|
+
const state = await readState(names.path, names.headPath, rootPath, names.assertStable, assertOwned);
|
|
252
|
+
const changed = purgeExpiredHostReceipts(state, current);
|
|
253
|
+
const receipt = makeHostTranscriptReceipt({ root: rootPath, binding: input.binding, source: snapshot,
|
|
254
|
+
transportDigest, current, eventId });
|
|
255
|
+
const existing = state.pendingReceipts.find((item) => sameIssuedHostTranscriptEvent(item, receipt));
|
|
256
|
+
if (existing) {
|
|
257
|
+
if (changed) await saveState(state, names.path, names.headPath, rootPath, assertOwned, names.assertStable);
|
|
258
|
+
return hostReceiptResult(existing);
|
|
259
|
+
}
|
|
260
|
+
if (state.pendingReceipts.length >= MAX_PENDING_RECEIPTS) {
|
|
261
|
+
if (changed) await saveState(state, names.path, names.headPath, rootPath, assertOwned, names.assertStable);
|
|
262
|
+
return hostReceiptUnavailable("host-receipt-capacity-exhausted");
|
|
263
|
+
}
|
|
264
|
+
state.pendingReceipts.unshift(receipt);
|
|
265
|
+
await saveState(state, names.path, names.headPath, rootPath, assertOwned, names.assertStable);
|
|
266
|
+
return hostReceiptResult(receipt);
|
|
267
|
+
}, { assertPath: names.assertStable });
|
|
268
|
+
} catch {
|
|
269
|
+
return hostReceiptUnavailable("host-receipt-state-unavailable");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// The local owner can retrieve only the opaque current-session token. The
|
|
274
|
+
// protected transport capability proves the caller's host session; no source
|
|
275
|
+
// path, scope, or transcript content is returned to a generic CLI process.
|
|
276
|
+
export async function currentHostTranscriptReceipt({ root, clock = null, environment = process.env }) {
|
|
277
|
+
const current = runtimeDate(clock);
|
|
278
|
+
if (!current) return hostReceiptUnavailable("invalid-host-receipt-time");
|
|
279
|
+
const rootPath = await canonicalRoot(root);
|
|
280
|
+
if (!rootPath) return hostReceiptUnavailable("unknown-project-root");
|
|
281
|
+
try {
|
|
282
|
+
const { state } = await loadStateWithRecovery(rootPath);
|
|
283
|
+
const matches = state.pendingReceipts.filter((item) => {
|
|
284
|
+
if (new Date(item.expiresAt).getTime() <= current.getTime()) return false;
|
|
285
|
+
const transportDigest = timelineTransportDigest({ root: rootPath, binding: item.binding, environment });
|
|
286
|
+
return transportDigest && sameTimelineTransportDigest(item.transportDigest, transportDigest);
|
|
287
|
+
});
|
|
288
|
+
return matches.length === 1 ? hostReceiptResult(matches[0])
|
|
289
|
+
: hostReceiptUnavailable(matches.length ? "host-receipt-ambiguous" : "host-receipt-unavailable");
|
|
290
|
+
} catch {
|
|
291
|
+
return hostReceiptUnavailable("host-receipt-state-unavailable");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Internal state-only lookup for bootstrap and lifecycle hints. It reads the
|
|
296
|
+
// signed enrollment record but deliberately never opens, stats, or hashes the
|
|
297
|
+
// transcript; callers still need the normal resolver before any evidence read.
|
|
298
|
+
export async function loadPrivateSessionTimelineEnrollment({
|
|
299
|
+
root, host = null, sessionId = null, scope = null, enrollmentDigest: wantedDigest = null,
|
|
300
|
+
expectedTransportDigest = null, clock = null
|
|
301
|
+
}) {
|
|
302
|
+
const current = runtimeDate(clock);
|
|
303
|
+
if (!current) return inspectUnavailable("invalid-enrollment-time");
|
|
304
|
+
const rootPath = await canonicalRoot(root);
|
|
305
|
+
if (!rootPath || (wantedDigest !== null && !/^[a-f0-9]{64}$/.test(wantedDigest))) {
|
|
306
|
+
return inspectUnavailable("private-enrollment-unavailable");
|
|
307
|
+
}
|
|
308
|
+
const requested = scope === null ? null : validHostReceiptInput({ host, sessionId, scope });
|
|
309
|
+
if ((scope !== null || host !== null || sessionId !== null) && !requested?.binding) {
|
|
310
|
+
return inspectUnavailable("private-enrollment-unavailable");
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const { state } = await loadStateWithRecovery(rootPath);
|
|
314
|
+
const matches = state.enrollments.filter((record) => (!requested || sameTimelineBinding(record.binding, requested.binding))
|
|
315
|
+
&& (wantedDigest === null || record.enrollmentDigest === wantedDigest));
|
|
316
|
+
if (matches.length !== 1 || new Date(matches[0].expiresAt).getTime() <= current.getTime()) {
|
|
317
|
+
return inspectUnavailable("private-enrollment-unavailable");
|
|
318
|
+
}
|
|
319
|
+
const [record] = matches;
|
|
320
|
+
if (expectedTransportDigest !== null && (!validTimelineTransportDigest(expectedTransportDigest)
|
|
321
|
+
|| !sameTimelineTransportDigest(record.transportDigest, expectedTransportDigest))) {
|
|
322
|
+
return inspectUnavailable("private-enrollment-transport-mismatch");
|
|
323
|
+
}
|
|
324
|
+
return { status: "loaded", rootPath, record, now: current };
|
|
325
|
+
} catch {
|
|
326
|
+
return inspectUnavailable("private-enrollment-state-unavailable");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function activeResult(record, activeSource) {
|
|
331
|
+
return {
|
|
332
|
+
schema: PRIVATE_TIMELINE_ENROLLMENT_SCHEMA, status: "enrolled", id: record.id,
|
|
333
|
+
binding: { ...record.binding }, source: {
|
|
334
|
+
path: activeSource.path, profileRoot: activeSource.profileRoot, projectsRoot: activeSource.projectsRoot,
|
|
335
|
+
pathDigest: activeSource.pathDigest, identity: activeSource.identity, size: activeSource.size,
|
|
336
|
+
mtimeNs: activeSource.mtimeNs, ctimeNs: activeSource.ctimeNs,
|
|
337
|
+
commitmentDigest: null, committedBytes: 0
|
|
338
|
+
}, sourceDigest: sourceDigest(record.source), enrollmentDigest: record.enrollmentDigest,
|
|
339
|
+
expiresAt: record.expiresAt, timelineVisibility: record.timelineVisibility, authority: AUTHORITY
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function inspectUnavailable(reason) { return { status: "unavailable", reason }; }
|
|
344
|
+
|
|
345
|
+
export async function inspectPrivateSessionTimelineEnrollment({
|
|
346
|
+
root, host, sessionId, transcriptPath, hostHome, clock = null, expectedTransportDigest = null
|
|
347
|
+
}) {
|
|
348
|
+
const current = runtimeDate(clock);
|
|
349
|
+
if (!current) return inspectUnavailable("invalid-enrollment-time");
|
|
350
|
+
const rootPath = await canonicalRoot(root);
|
|
351
|
+
if (!rootPath) return inspectUnavailable("unknown-project-root");
|
|
352
|
+
const activeSessionId = validResolverInput({ host, sessionId });
|
|
353
|
+
if (!activeSessionId) return inspectUnavailable("unknown-timeline-host-or-session");
|
|
354
|
+
try {
|
|
355
|
+
const { state } = await loadStateWithRecovery(rootPath);
|
|
356
|
+
const candidates = state.enrollments.filter((item) => item.binding.host === host && item.binding.sessionId === activeSessionId);
|
|
357
|
+
const requestedPath = typeof transcriptPath === "string" && transcriptPath
|
|
358
|
+
? transcriptPath : candidates.length === 1 ? candidates[0].source.path : null;
|
|
359
|
+
// Post-compaction input may omit a host profile root. In that case, the
|
|
360
|
+
// sealed root from the one matching enrollment is the only allowed
|
|
361
|
+
// fallback; an environment or arbitrary caller path is never consulted.
|
|
362
|
+
const trustedHome = hostHome === null || hostHome === undefined
|
|
363
|
+
? candidates.length === 1 ? candidates[0].source.profileRoot : null
|
|
364
|
+
: isAbsolute(hostHome) ? hostHome : null;
|
|
365
|
+
if (!requestedPath || !trustedHome) return inspectUnavailable("private-enrollment-unavailable");
|
|
366
|
+
const active = await sourcePath(requestedPath, trustedHome, host);
|
|
367
|
+
if (active.status !== "registered") return inspectUnavailable(active.reason || "transcript-unavailable");
|
|
368
|
+
const matches = candidates.filter((item) => sameSource(item.source, active));
|
|
369
|
+
if (matches.length !== 1) return inspectUnavailable("private-enrollment-unavailable");
|
|
370
|
+
const [record] = matches;
|
|
371
|
+
if (new Date(record.expiresAt).getTime() <= current.getTime()) return inspectUnavailable("private-enrollment-expired");
|
|
372
|
+
if (expectedTransportDigest !== null && (!validTimelineTransportDigest(expectedTransportDigest)
|
|
373
|
+
|| !sameTimelineTransportDigest(record.transportDigest, expectedTransportDigest))) {
|
|
374
|
+
return inspectUnavailable("private-enrollment-transport-mismatch");
|
|
375
|
+
}
|
|
376
|
+
return { status: "loaded", rootPath, record, active, now: current };
|
|
377
|
+
} catch {
|
|
378
|
+
return inspectUnavailable("private-enrollment-state-unavailable");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function mutatePrivateSessionTimelineEnrollment({ root, task }) {
|
|
383
|
+
const rootPath = await canonicalRoot(root);
|
|
384
|
+
if (!rootPath || typeof task !== "function") return null;
|
|
385
|
+
try {
|
|
386
|
+
const names = await enrollmentPaths(rootPath);
|
|
387
|
+
return await withOwnedFileLock(names.lock, async ({ assertOwned }) => {
|
|
388
|
+
const state = await readState(names.path, names.headPath, rootPath, names.assertStable, assertOwned);
|
|
389
|
+
let committed = false;
|
|
390
|
+
const commit = async () => {
|
|
391
|
+
if (committed) return;
|
|
392
|
+
await saveState(state, names.path, names.headPath, rootPath, assertOwned, names.assertStable);
|
|
393
|
+
committed = true;
|
|
394
|
+
};
|
|
395
|
+
return task({ rootPath, state, commit });
|
|
396
|
+
}, { assertPath: names.assertStable });
|
|
397
|
+
} catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// A first state write interrupted before any signed head exists is not
|
|
403
|
+
// distinguishable from a deleted head. It is never auto-accepted: a local
|
|
404
|
+
// owner may explicitly discard all enrollment state, then obtain a fresh host
|
|
405
|
+
// receipt. This function exposes no historic binding, path, or transcript.
|
|
406
|
+
export async function resetPrivateSessionTimelineEnrollment({ root, confirmation }) {
|
|
407
|
+
if (confirmation !== LOCAL_TIMELINE_ENROLLMENT_RECOVERY_CONFIRMATION) return false;
|
|
408
|
+
const rootPath = await canonicalRoot(root);
|
|
409
|
+
if (!rootPath) return false;
|
|
410
|
+
try {
|
|
411
|
+
const names = await enrollmentPaths(rootPath);
|
|
412
|
+
return await withOwnedFileLock(names.lock, async ({ assertOwned }) => {
|
|
413
|
+
try {
|
|
414
|
+
await readState(names.path, names.headPath, rootPath, names.assertStable, assertOwned);
|
|
415
|
+
return false;
|
|
416
|
+
} catch {
|
|
417
|
+
const state = empty(rootPath);
|
|
418
|
+
await saveState(state, names.path, names.headPath, rootPath, assertOwned, names.assertStable);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}, { assertPath: names.assertStable });
|
|
422
|
+
} catch {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export async function enrollPrivateSessionTimeline(options = {}) {
|
|
428
|
+
const { root, hostReceipt, confirmation, ttlMs = DEFAULT_TIMELINE_ENROLLMENT_TTL_MS,
|
|
429
|
+
clock = null, environment = process.env } = options || {};
|
|
430
|
+
if (!options || typeof options !== "object" || Array.isArray(options)
|
|
431
|
+
|| ["host", "sessionId", "scope", "transcriptPath", "hostHome"].some((key) => options[key] !== undefined)) {
|
|
432
|
+
return unavailable("host-transcript-receipt-required");
|
|
433
|
+
}
|
|
434
|
+
if (confirmation !== LOCAL_TIMELINE_ENROLLMENT_CONFIRMATION) return unavailable("local-owner-confirmation-required");
|
|
435
|
+
if (!hostReceiptId(hostReceipt)) return unavailable("host-transcript-receipt-required");
|
|
436
|
+
const current = runtimeDate(clock);
|
|
437
|
+
if (!current) return unavailable("invalid-enrollment-time");
|
|
438
|
+
const ttl = enrollmentTtl(ttlMs);
|
|
439
|
+
if (!ttl) return unavailable("invalid-enrollment-expiry");
|
|
440
|
+
const rootPath = await canonicalRoot(root);
|
|
441
|
+
if (!rootPath) return unavailable("unknown-project-root");
|
|
442
|
+
try {
|
|
443
|
+
const names = await enrollmentPaths(rootPath);
|
|
444
|
+
let result = unavailable("host-transcript-receipt-unavailable");
|
|
445
|
+
await withOwnedFileLock(names.lock, async ({ assertOwned }) => {
|
|
446
|
+
const state = await readState(names.path, names.headPath, rootPath, names.assertStable, assertOwned);
|
|
447
|
+
const pruned = purgeExpiredHostReceipts(state, current);
|
|
448
|
+
const receiptIndex = state.pendingReceipts.findIndex((item) => item.id === hostReceipt);
|
|
449
|
+
const receipt = receiptIndex < 0 ? null : state.pendingReceipts[receiptIndex];
|
|
450
|
+
if (!receipt) {
|
|
451
|
+
if (pruned) await saveState(state, names.path, names.headPath, rootPath, assertOwned, names.assertStable);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const transportDigest = timelineTransportDigest({ root: rootPath, binding: receipt.binding, environment });
|
|
455
|
+
if (!transportDigest || !sameTimelineTransportDigest(receipt.transportDigest, transportDigest)) {
|
|
456
|
+
result = unavailable("local-transport-capability-required"); return;
|
|
457
|
+
}
|
|
458
|
+
const source = await sourcePath(receipt.source.path, receipt.source.profileRoot, receipt.binding.host);
|
|
459
|
+
const normalized = source.status === "registered" ? await normalizePrivateTimelineSource(source) : null;
|
|
460
|
+
state.pendingReceipts.splice(receiptIndex, 1);
|
|
461
|
+
if (!normalized || !sameHostReceiptSource(receipt, normalized)) {
|
|
462
|
+
await saveState(state, names.path, names.headPath, rootPath, assertOwned, names.assertStable);
|
|
463
|
+
result = unavailable("transcript-snapshot-changed"); return;
|
|
464
|
+
}
|
|
465
|
+
const record = { binding: receipt.binding, source: normalized, confirmedAt: current.toISOString(),
|
|
466
|
+
expiresAt: new Date(current.getTime() + ttl).toISOString(), timelineVisibility: TIMELINE_VISIBILITY,
|
|
467
|
+
confirmation: LOCAL_TIMELINE_ENROLLMENT_CONFIRMATION, transportDigest: receipt.transportDigest, authority: AUTHORITY };
|
|
468
|
+
record.id = enrollmentId(rootPath, record.binding, record.source);
|
|
469
|
+
record.enrollmentDigest = enrollmentDigest(rootPath, record.binding, record.source, record.expiresAt, record.transportDigest);
|
|
470
|
+
state.enrollments = state.enrollments.filter((item) => new Date(item.expiresAt).getTime() > current.getTime()
|
|
471
|
+
&& !(sameTimelineBinding(item.binding, record.binding)
|
|
472
|
+
|| (item.binding.host === record.binding.host && item.binding.sessionId === record.binding.sessionId
|
|
473
|
+
&& sameSource(item.source, record.source))));
|
|
474
|
+
state.enrollments.unshift(record);
|
|
475
|
+
state.enrollments = state.enrollments.slice(0, MAX_ENROLLMENTS);
|
|
476
|
+
await saveState(state, names.path, names.headPath, rootPath, assertOwned, names.assertStable);
|
|
477
|
+
result = activeResult(record, source);
|
|
478
|
+
}, { assertPath: names.assertStable });
|
|
479
|
+
return result;
|
|
480
|
+
} catch {
|
|
481
|
+
return unavailable("private-enrollment-state-unavailable");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export async function resolvePrivateSessionTimelineEnrollment({
|
|
486
|
+
root, host, sessionId, transcriptPath, hostHome, clock = null, expectedTransportDigest = null
|
|
487
|
+
}) {
|
|
488
|
+
const inspected = await inspectPrivateSessionTimelineEnrollment({
|
|
489
|
+
root, host, sessionId, transcriptPath, hostHome, clock, expectedTransportDigest
|
|
490
|
+
});
|
|
491
|
+
if (inspected.status !== "loaded") return unavailable(inspected.reason);
|
|
492
|
+
const { record, active } = inspected;
|
|
493
|
+
if (!sameSourceSnapshot(record.source, active)) {
|
|
494
|
+
return unavailable("transcript-snapshot-renewal-required");
|
|
495
|
+
}
|
|
496
|
+
if (await privateTimelinePrefixDigest(active, record.source.prefixBytes) !== record.source.prefixDigest) {
|
|
497
|
+
return unavailable("transcript-replaced");
|
|
498
|
+
}
|
|
499
|
+
return activeResult(record, active);
|
|
500
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { codexTimelineToolResult } from "./session-timeline-codex.js";
|
|
2
|
+
import { kingTimelineToolResult } from "./session-timeline-king.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { timelineTerms } from "./session-timeline-query.js";
|
|
5
|
+
|
|
6
|
+
const MAX_LINE_BYTES = 1024 * 1024;
|
|
7
|
+
const TEXT_KEY_RE = /^(?:content|text|result|output|error|message)$/i;
|
|
8
|
+
const SENSITIVE_KEY_RE = /^(?:api[_-]?key|apikey|access[_-]?token|refresh[_-]?token|token|secret|client[_-]?secret|password|passwort|credential|authorization|proxy[_-]?authorization|cookie|set[_-]?cookie|private[_-]?key|aws[_-]?secret[_-]?access[_-]?key)$/i;
|
|
9
|
+
const SECRET_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)|\b(?:aspt|astp|astc)_[A-Za-z0-9_.-]{40,}\b|\b(?:sk[-_](?:proj[-_])?|gh[opusu]_)[A-Za-z0-9_-]{20,}\b|\b(?:xox[bapcrs]-|github_pat_|glpat-|npm_)[A-Za-z0-9_-]{12,}\b|\bAKIA[0-9A-Z]{16}\b|\b[a-z][a-z0-9+.-]{0,31}:\/\/[^\s\/@:]+:[^\s\/@]+@|https?:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_/-]+|\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/i;
|
|
10
|
+
const CREDENTIAL_VALUE_RE = /["'](?:api[_-]?key|apikey|access[_-]?token|refresh[_-]?token|token|secret|client[_-]?secret|password|passwort|credential|authorization|proxy[_-]?authorization|cookie|set[_-]?cookie|private[_-]?key|aws[_-]?secret[_-]?access[_-]?key)["']\s*:\s*(?:["'][^"'\r\n]*["']|[^,}\]\s]+)|\b(?:api[_-]?key|apikey|access[_-]?token|refresh[_-]?token|token|secret|client[_-]?secret|password|passwort|credential|aws[_-]?secret[_-]?access[_-]?key)\s*[:=]\s*(?:["'][^"'\r\n]*["']|\S+)/i;
|
|
11
|
+
const AUTHORIZATION_RE = /\b(?:authorization|proxy-authorization)\s*:\s*(?:bearer|basic|digest|token|apikey|api-key|aws4-hmac-sha256)\b|\b(?:bearer|basic|digest|token|apikey|api-key|aws4-hmac-sha256)\s+[A-Za-z0-9._~+\/=:-]+/i;
|
|
12
|
+
const INSTRUCTION_RE = /(?:\b(?:ignore|disregard|override|bypass|forget)\b[\s\S]{0,180}\b(?:previous|prior|system|developer|instruction|instructions|prompt|prompts|rule|rules|safeguard|safeguards)\b|\b(?:ignoriere|missachte|übergehe|umgehe|vergesse)\b[\s\S]{0,180}\b(?:vorherige|frühere|system|entwickler|anweisung|anweisungen|prompt|prompts|regel|regeln|schutzmaßnahme|schutzmaßnahmen)\b|<\s*\/?\s*(?:system|developer|assistant|instructions?|prompt)\b[^>]*>)/i;
|
|
13
|
+
const OBJECTIVE_RE = /\b(?:measur(?:e|ed|ement)|gemessen|benchmark|test(?:ed|ing)?|suite|assert(?:ion)?|ci\b|prüfung|durchlauf|ergebnis)\b/i;
|
|
14
|
+
const OUTCOME_PATTERNS = [
|
|
15
|
+
["pass", /\b(?:pass(?:ed|ing)?|green|success(?:ful)?|ok|bestanden|erfolgreich)\b/gi],
|
|
16
|
+
["fail", /\b(?:fail(?:ed|ure)?|red|fehlgeschlagen)\b/gi],
|
|
17
|
+
["blocked", /\b(?:blocked|blockiert)\b/gi],
|
|
18
|
+
["timeout", /\b(?:timeout|zeitüberschreitung)\b/gi],
|
|
19
|
+
["error", /\b(?:error|fehler)\b/gi],
|
|
20
|
+
["skipped", /\b(?:skipped|übersprungen)\b/gi]
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function digest(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
24
|
+
|
|
25
|
+
export function extractTimelineTimestamp(value) {
|
|
26
|
+
for (const key of ["timestamp", "created_at", "createdAt"]) {
|
|
27
|
+
const current = value?.[key];
|
|
28
|
+
if (typeof current !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?Z$/.test(current)) continue;
|
|
29
|
+
const parsed = new Date(current);
|
|
30
|
+
if (Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 19) === current.slice(0, 19)) return parsed.toISOString();
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isToolResult(value) {
|
|
36
|
+
const type = String(value?.type || value?.kind || "").toLowerCase().replaceAll("_", "-");
|
|
37
|
+
return value?.role === "tool" || ["tool-result", "toolresult", "command-result", "runner-result"].includes(type);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unsafeText(value) {
|
|
41
|
+
return SECRET_RE.test(value) || CREDENTIAL_VALUE_RE.test(value) || AUTHORIZATION_RE.test(value) || INSTRUCTION_RE.test(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function unsafeObject(value) {
|
|
45
|
+
const pending = [{ value, depth: 0 }];
|
|
46
|
+
let visited = 0;
|
|
47
|
+
while (pending.length && visited < 128) {
|
|
48
|
+
const current = pending.pop();
|
|
49
|
+
visited += 1;
|
|
50
|
+
if (typeof current.value === "string") {
|
|
51
|
+
if (unsafeText(current.value)) return true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!current.value || typeof current.value !== "object" || current.depth > 5) continue;
|
|
55
|
+
for (const [key, item] of Object.entries(current.value).slice(0, 64)) {
|
|
56
|
+
if (SENSITIVE_KEY_RE.test(key)) return true;
|
|
57
|
+
pending.push({ value: item, depth: current.depth + 1 });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return visited >= 128;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function objectiveText(value) {
|
|
64
|
+
const values = [];
|
|
65
|
+
const visit = (item, key = "", depth = 0) => {
|
|
66
|
+
if (depth > 5 || values.length >= 12) return;
|
|
67
|
+
if (typeof item === "string") {
|
|
68
|
+
if (TEXT_KEY_RE.test(key)) values.push(item.slice(0, 4096));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!item || typeof item !== "object") return;
|
|
72
|
+
for (const [name, current] of Object.entries(item).slice(0, 48)) visit(current, name, depth + 1);
|
|
73
|
+
};
|
|
74
|
+
visit(value);
|
|
75
|
+
return values.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizedOutcome(value) {
|
|
79
|
+
const found = OUTCOME_PATTERNS.filter(([, pattern]) => {
|
|
80
|
+
pattern.lastIndex = 0;
|
|
81
|
+
return pattern.test(value);
|
|
82
|
+
}).map(([outcome]) => outcome);
|
|
83
|
+
return found.length === 1 ? found[0] : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizedCount(value) {
|
|
87
|
+
const matches = [...value.matchAll(/\b(\d{1,6})\s*\/\s*(\d{1,6})\b/g)].map((match) => ({
|
|
88
|
+
value: Number(match[1]), total: Number(match[2])
|
|
89
|
+
})).filter((item) => item.value <= item.total && item.total > 0);
|
|
90
|
+
if (!matches.length) return null;
|
|
91
|
+
const first = matches[0];
|
|
92
|
+
return matches.every((item) => item.value === first.value && item.total === first.total) ? first : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizedTestLabel(value) {
|
|
96
|
+
const suite = value.match(/\b(?:suite|test(?:\s+suite)?|testlauf|prüfung)\s*(?:#|nr\.?|nummer)?\s*(\d{1,4})\b/i);
|
|
97
|
+
if (suite) return `suite-${Number(suite[1])}`;
|
|
98
|
+
if (/\bacceptance\b/i.test(value)) return "acceptance";
|
|
99
|
+
if (/\baudit\b/i.test(value)) return "audit";
|
|
100
|
+
if (/\bnpm\s+run\s+check\b/i.test(value)) return "npm-check";
|
|
101
|
+
if (/\bci\b/i.test(value)) return "ci";
|
|
102
|
+
if (/\b(?:test|prüfung|durchlauf)\b/i.test(value)) return "test";
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function structuredTerms({ outcome, count, testLabel }) {
|
|
107
|
+
return timelineTerms(["objective", "measurement", "messung", "result", "ergebnis", outcome, testLabel || "",
|
|
108
|
+
count ? String(count.value) : "", count ? String(count.total) : ""].join(" "));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function objectiveExcerpt(body) {
|
|
112
|
+
const segments = body.split(/(?:\r?\n)+|(?<=[.!?])\s+/u).map((item) => item.trim()).filter(Boolean);
|
|
113
|
+
const selected = segments.find((item) => OBJECTIVE_RE.test(item) && normalizedOutcome(item));
|
|
114
|
+
return (selected || "").replace(/\s+/g, " ").slice(0, 320);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function candidateFromToolResult(value) {
|
|
118
|
+
if (!isToolResult(value) || unsafeObject(value)) return null;
|
|
119
|
+
const body = objectiveText(value);
|
|
120
|
+
if (!body || !OBJECTIVE_RE.test(body)) return null;
|
|
121
|
+
const outcome = normalizedOutcome(body);
|
|
122
|
+
if (!outcome) return null;
|
|
123
|
+
const count = normalizedCount(body);
|
|
124
|
+
const testLabel = normalizedTestLabel(body);
|
|
125
|
+
return { kind: "objective-result", outcome, count, testLabel, terms: structuredTerms({ outcome, count, testLabel }) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function timelineEventFromLine(line, offset, authority, includeExcerpt, host) {
|
|
129
|
+
const source = Buffer.from(line);
|
|
130
|
+
if (source.byteLength > MAX_LINE_BYTES) return null;
|
|
131
|
+
let parsed;
|
|
132
|
+
try { parsed = JSON.parse(line.trim()); } catch {
|
|
133
|
+
if (["codex", "king"].includes(host)) throw new Error(`${host}-history-format-mismatch`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const tool = host === "codex" ? codexTimelineToolResult(parsed)
|
|
137
|
+
: host === "king" ? kingTimelineToolResult(parsed) : null;
|
|
138
|
+
const at = tool?.at || extractTimelineTimestamp(parsed);
|
|
139
|
+
if (!at || unsafeObject(parsed)) return null;
|
|
140
|
+
const candidate = ["codex", "king"].includes(host) ? candidateFromToolResult(tool)
|
|
141
|
+
: candidateFromToolResult(parsed) || candidateFromToolResult(parsed.message);
|
|
142
|
+
if (!candidate) return null;
|
|
143
|
+
const stable = JSON.stringify({ at, ...candidate });
|
|
144
|
+
const event = { id: `timeline-event:${digest(`${offset}\0${stable}`).slice(0, 32)}`, at, offset, bytes: source.byteLength,
|
|
145
|
+
sha256: digest(source), ...candidate, authority };
|
|
146
|
+
if (tool) event.nativeMessageId = tool.nativeMessageId;
|
|
147
|
+
if (includeExcerpt) event.excerpt = objectiveExcerpt(objectiveText(tool || parsed));
|
|
148
|
+
return event;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function eventFromTimelineLine(line, offset, authority = "context-only", host = "claude") {
|
|
152
|
+
return timelineEventFromLine(line, offset, authority, false, host);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function verifiedTimelineEventFromLine(line, offset, authority = "context-only", host = "claude") {
|
|
156
|
+
return timelineEventFromLine(line, offset, authority, true, host);
|
|
157
|
+
}
|