@aiwg/cli 2026.9.20 → 2026.9.21
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/agentic/code/providers/capability-matrix.yaml +58 -6
- package/agentic/code/providers/model-capabilities.v1.json +34 -0
- package/agentic/code/providers/model-catalog.v1.json +55 -13
- package/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/enrichment/cli.js +1 -1
- package/dist/src/artifacts/views/cli.js +1 -1
- package/dist/src/audit/operator-decision.js +10 -0
- package/dist/src/auth/credential-store.js +127 -27
- package/dist/src/catalog/builtin-models.json +272 -40
- package/dist/src/catalog/cli.js +1 -1
- package/dist/src/catalog/cli.mjs +1 -1
- package/dist/src/cli/handlers/effect.js +872 -0
- package/dist/src/cli/handlers/help.js +2 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/models.js +41 -10
- package/dist/src/cli/handlers/ralph.js +1 -1
- package/dist/src/cli/handlers/sessions.js +113 -37
- package/dist/src/cli/handlers/subcommands.js +31 -0
- package/dist/src/cli/handlers/use.js +46 -9
- package/dist/src/cli/handlers/utilities.js +10 -0
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/scope-resolver.js +112 -9
- package/dist/src/cli/services/deployment-verification.js +5 -0
- package/dist/src/config/aiwg-config.js +3 -0
- package/dist/src/decision/acceptance-rollout.js +93 -0
- package/dist/src/decision/acceptance.js +175 -0
- package/dist/src/decision/adapters/jev.js +234 -13
- package/dist/src/decision/adapters/llm-subagent.js +60 -12
- package/dist/src/decision/admission.js +550 -0
- package/dist/src/decision/batch-benchmark.js +47 -0
- package/dist/src/decision/batch-receipts/accounting.js +75 -0
- package/dist/src/decision/batch-receipts/export.js +40 -0
- package/dist/src/decision/batch-receipts/index.js +9 -0
- package/dist/src/decision/batch-receipts/protection.js +40 -0
- package/dist/src/decision/batch-receipts/receipt.js +44 -0
- package/dist/src/decision/batch-receipts/result-store.js +341 -0
- package/dist/src/decision/batch-receipts/store.js +362 -0
- package/dist/src/decision/batch-receipts/types.js +2 -0
- package/dist/src/decision/batch-receipts/validate.js +127 -0
- package/dist/src/decision/batch.js +68 -0
- package/dist/src/decision/calibration/index.js +4 -0
- package/dist/src/decision/calibration/receipts.js +130 -0
- package/dist/src/decision/calibration/registry.js +258 -0
- package/dist/src/decision/calibration/types.js +2 -0
- package/dist/src/decision/compile-cache/benchmark.js +62 -0
- package/dist/src/decision/compile-cache/compatibility.js +65 -0
- package/dist/src/decision/compile-cache/file-store.js +280 -0
- package/dist/src/decision/compile-cache/identity.js +26 -0
- package/dist/src/decision/compile-cache/index.js +9 -0
- package/dist/src/decision/compile-cache/prefix.js +21 -0
- package/dist/src/decision/compile-cache/records.js +86 -0
- package/dist/src/decision/compile-cache/runtime.js +68 -0
- package/dist/src/decision/compile-cache/store.js +108 -0
- package/dist/src/decision/compile-cache/types.js +2 -0
- package/dist/src/decision/context-plan.js +234 -0
- package/dist/src/decision/context-qualification.js +60 -0
- package/dist/src/decision/ensemble/aggregate.js +215 -0
- package/dist/src/decision/ensemble/contract.js +316 -0
- package/dist/src/decision/ensemble/drift.js +51 -0
- package/dist/src/decision/ensemble/index.js +6 -0
- package/dist/src/decision/ensemble/report.js +71 -0
- package/dist/src/decision/ensemble/types.js +2 -0
- package/dist/src/decision/evaluate.js +1024 -78
- package/dist/src/decision/file-lifecycle-store.js +168 -0
- package/dist/src/decision/graph-beam.js +40 -0
- package/dist/src/decision/graph-budget.js +110 -0
- package/dist/src/decision/graph-decision-bridge.js +93 -0
- package/dist/src/decision/graph-evidence.js +123 -0
- package/dist/src/decision/graph-flow-adapter.js +56 -0
- package/dist/src/decision/graph-flow.js +105 -0
- package/dist/src/decision/graph-parallel.js +79 -0
- package/dist/src/decision/graph-patterns.js +20 -0
- package/dist/src/decision/graph-run-store.js +66 -0
- package/dist/src/decision/graph-run.js +166 -0
- package/dist/src/decision/graph-skill-bridge.js +91 -0
- package/dist/src/decision/graph-templates.js +56 -0
- package/dist/src/decision/graph.js +115 -0
- package/dist/src/decision/index.js +35 -0
- package/dist/src/decision/job-accounting.js +93 -0
- package/dist/src/decision/job-contract.js +186 -0
- package/dist/src/decision/job-effects.js +95 -0
- package/dist/src/decision/job-evaluate.js +80 -0
- package/dist/src/decision/job-gateway.js +121 -0
- package/dist/src/decision/job-http.js +194 -0
- package/dist/src/decision/job-payload-store.js +258 -0
- package/dist/src/decision/job-poll-limiter.js +92 -0
- package/dist/src/decision/job-quota.js +158 -0
- package/dist/src/decision/job-runtime.js +240 -0
- package/dist/src/decision/job-scheduler.js +26 -0
- package/dist/src/decision/job-service.js +117 -0
- package/dist/src/decision/job-store.js +264 -0
- package/dist/src/decision/job-worker.js +141 -0
- package/dist/src/decision/lifecycle.js +148 -0
- package/dist/src/decision/patterns/artifacts.js +173 -0
- package/dist/src/decision/patterns/catalog.js +108 -0
- package/dist/src/decision/patterns/durable-review.js +237 -0
- package/dist/src/decision/patterns/index.js +8 -0
- package/dist/src/decision/patterns/playground.js +401 -0
- package/dist/src/decision/patterns/recorded.js +43 -0
- package/dist/src/decision/patterns/specs.js +174 -0
- package/dist/src/decision/patterns/types.js +2 -0
- package/dist/src/decision/portable-secrets.js +53 -0
- package/dist/src/decision/projection.js +210 -0
- package/dist/src/decision/qualification/capture.js +67 -0
- package/dist/src/decision/qualification/drift.js +68 -0
- package/dist/src/decision/qualification/gate-evidence.js +172 -0
- package/dist/src/decision/qualification/gates.js +83 -0
- package/dist/src/decision/qualification/index.js +12 -0
- package/dist/src/decision/qualification/load.js +275 -0
- package/dist/src/decision/qualification/manifest.js +37 -0
- package/dist/src/decision/qualification/privacy.js +41 -0
- package/dist/src/decision/qualification/quality.js +213 -0
- package/dist/src/decision/qualification/release.js +116 -0
- package/dist/src/decision/qualification/runner.js +408 -0
- package/dist/src/decision/qualification/types.js +2 -0
- package/dist/src/decision/receipts.js +30 -7
- package/dist/src/decision/result-cache/index.js +6 -0
- package/dist/src/decision/result-cache/integrity.js +28 -0
- package/dist/src/decision/result-cache/key.js +28 -0
- package/dist/src/decision/result-cache/service.js +92 -0
- package/dist/src/decision/result-cache/store.js +327 -0
- package/dist/src/decision/result-cache/types.js +3 -0
- package/dist/src/decision/review/authorization.js +110 -0
- package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
- package/dist/src/decision/review/index.js +12 -0
- package/dist/src/decision/review/migrate.js +34 -0
- package/dist/src/decision/review/operator-audit.js +59 -0
- package/dist/src/decision/review/recovery-adapters.js +252 -0
- package/dist/src/decision/review/recovery.js +33 -0
- package/dist/src/decision/review/service.js +498 -0
- package/dist/src/decision/review/store.js +219 -0
- package/dist/src/decision/review/types.js +2 -0
- package/dist/src/decision/review/validate.js +205 -0
- package/dist/src/decision/scheduler.js +115 -0
- package/dist/src/decision/telemetry/context.js +42 -0
- package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
- package/dist/src/decision/telemetry/exporter.js +77 -0
- package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
- package/dist/src/decision/telemetry/index.js +13 -0
- package/dist/src/decision/telemetry/mapping.js +146 -0
- package/dist/src/decision/telemetry/metrics.js +164 -0
- package/dist/src/decision/telemetry/otlp-http.js +183 -0
- package/dist/src/decision/telemetry/redaction.js +93 -0
- package/dist/src/decision/telemetry/retention.js +109 -0
- package/dist/src/decision/telemetry/runtime.js +310 -0
- package/dist/src/decision/telemetry/trace.js +158 -0
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/validate.js +135 -5
- package/dist/src/effects/checkpoint-sinks.js +94 -0
- package/dist/src/effects/checkpoint.js +76 -0
- package/dist/src/effects/decision-links.js +54 -0
- package/dist/src/effects/errors.js +48 -0
- package/dist/src/effects/identity.js +178 -0
- package/dist/src/effects/index.js +25 -0
- package/dist/src/effects/keyring.js +160 -0
- package/dist/src/effects/keys.js +130 -0
- package/dist/src/effects/ledger.js +435 -0
- package/dist/src/effects/lifecycle.js +110 -0
- package/dist/src/effects/lock-recovery.js +269 -0
- package/dist/src/effects/reader.js +103 -0
- package/dist/src/effects/records.js +114 -0
- package/dist/src/effects/redaction.js +51 -0
- package/dist/src/effects/schema.js +73 -0
- package/dist/src/effects/store.js +239 -0
- package/dist/src/effects/types.js +49 -0
- package/dist/src/effects/verifiers/decision.js +168 -0
- package/dist/src/effects/verifiers/file.js +106 -0
- package/dist/src/effects/verifiers/git.js +206 -0
- package/dist/src/effects/verifiers/index.js +90 -0
- package/dist/src/effects/verifiers/registry.js +148 -0
- package/dist/src/effects/verifiers/review.js +123 -0
- package/dist/src/effects/verifiers/tracker.js +299 -0
- package/dist/src/effects/verifiers/types.js +21 -0
- package/dist/src/effects/verify.js +295 -0
- package/dist/src/evidence/bundle.js +2 -0
- package/dist/src/extensions/commands/definitions.js +28 -0
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/marketplace/provenance.js +25 -27
- package/dist/src/mcp/cli.mjs +11 -1
- package/dist/src/mcp/grok-build-config.mjs +189 -0
- package/dist/src/mcp/registry.js +21 -1
- package/dist/src/mcp/registry.mjs +20 -1
- package/dist/src/models/grok-build-models.js +356 -0
- package/dist/src/models/index.js +1 -0
- package/dist/src/models/model-capabilities.v1.json +34 -0
- package/dist/src/models/model-catalog.v1.json +55 -13
- package/dist/src/models/model-discovery.js +41 -5
- package/dist/src/models/provider-policy.js +3 -2
- package/dist/src/plugin/skill-command-translator.js +1 -0
- package/dist/src/providers/capability-matrix.yaml +58 -6
- package/dist/src/providers/grok-build-acp.js +192 -0
- package/dist/src/providers/grok-build-dispatch.js +251 -0
- package/dist/src/providers/grok-build-headless.js +170 -0
- package/dist/src/providers/muse-paths.js +105 -0
- package/dist/src/providers/provider-definitions.js +93 -7
- package/dist/src/providers/provider-definitions.mjs +21 -1
- package/dist/src/providers/provider-inventory.js +3 -0
- package/dist/src/security/artifact-attestation.js +4 -8
- package/dist/src/security/signing.js +119 -0
- package/dist/src/sessions/adapters/grok-build.js +187 -0
- package/dist/src/sessions/adapters/muse.js +412 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/grok-build-cli.js +196 -0
- package/dist/src/sessions/index.js +3 -0
- package/dist/src/sessions/workspace-discovery.js +9 -5
- package/dist/src/skills/deployer.js +33 -5
- package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
- package/dist/src/smiths/context-pipeline/finalization.js +1 -32
- package/dist/src/smiths/context-pipeline/generator.js +12 -0
- package/dist/src/smiths/context-pipeline/index.js +1 -1
- package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
- package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
- package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
- package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
- package/dist/src/smiths/hook-bridge/index.js +3 -0
- package/dist/src/smiths/hook-bridge/shim.js +2 -0
- package/dist/src/smiths/hook-bridge/types.js +5 -0
- package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
- package/dist/src/storage/protected-files.js +102 -0
- package/dist/src/tracker/forge-http.js +371 -0
- package/dist/src/tracker/project-inputs.js +41 -0
- package/package.json +2 -1
- package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
- package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
- package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
- package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
- package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
- package/schemas/decision/DecisionBinding.schema.json +309 -0
- package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
- package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
- package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
- package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
- package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
- package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
- package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
- package/schemas/decision/DecisionDefinition.schema.json +159 -0
- package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
- package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
- package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
- package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
- package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
- package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
- package/schemas/decision/DecisionJob.v1.schema.json +71 -0
- package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
- package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
- package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
- package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
- package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
- package/schemas/decision/DecisionResult.schema.json +524 -0
- package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
- package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
- package/schemas/decision/DecisionReview.schema.json +29 -0
- package/schemas/decision/DecisionRuleset.schema.json +313 -0
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
- package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
- package/schemas/decision/RulesetResult.schema.json +721 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
- package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
- package/tools/agents/deploy-agents.mjs +21 -6
- package/tools/agents/providers/base.mjs +7 -4
- package/tools/agents/providers/codex.mjs +1 -1
- package/tools/agents/providers/grok-build.d.mts +7 -0
- package/tools/agents/providers/grok-build.mjs +249 -9
- package/tools/agents/providers/model-role.mjs +6 -3
- package/tools/agents/providers/muse-hooks.mjs +785 -0
- package/tools/agents/providers/muse-paths.mjs +112 -0
- package/tools/agents/providers/muse.mjs +336 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
3
|
+
export function digestResultCacheIdentity(identity) {
|
|
4
|
+
return resultCacheSha256(canonicalJson(normalizeSemanticStrings(identity)));
|
|
5
|
+
}
|
|
6
|
+
export function digestCachedResult(value) { return resultCacheSha256(canonicalJson(value)); }
|
|
7
|
+
/** NFC normalizes equivalent Unicode input without conflating distinct object keys. */
|
|
8
|
+
function normalizeSemanticStrings(value) {
|
|
9
|
+
if (typeof value === 'string')
|
|
10
|
+
return value.normalize('NFC');
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.map(normalizeSemanticStrings);
|
|
13
|
+
if (value !== null && typeof value === 'object') {
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [rawKey, child] of Object.entries(value)) {
|
|
16
|
+
const key = rawKey.normalize('NFC');
|
|
17
|
+
if (Object.hasOwn(result, key))
|
|
18
|
+
throw new Error('Ambiguous Unicode keys in semantic identity');
|
|
19
|
+
result[key] = normalizeSemanticStrings(child);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function resultCacheSha256(value) {
|
|
26
|
+
return `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=key.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { digestCachedResult, digestResultCacheIdentity } from './key.js';
|
|
3
|
+
import { entryIntegrityDigest } from './integrity.js';
|
|
4
|
+
export class DecisionResultCache {
|
|
5
|
+
store;
|
|
6
|
+
telemetry;
|
|
7
|
+
flights = new Map();
|
|
8
|
+
constructor(store, telemetry = () => { }) {
|
|
9
|
+
this.store = store;
|
|
10
|
+
this.telemetry = telemetry;
|
|
11
|
+
}
|
|
12
|
+
async evaluate(request, fill) {
|
|
13
|
+
const now = request.nowEpochMs ?? Date.now();
|
|
14
|
+
const operationId = request.operationId ?? randomUUID();
|
|
15
|
+
if (!cacheable(request.policy))
|
|
16
|
+
return this.bypass(request, fill, now, operationId, 'policy-disabled');
|
|
17
|
+
if (!modelReusable(request.identity, now))
|
|
18
|
+
return this.bypass(request, fill, now, operationId, 'model-compatibility-unproven');
|
|
19
|
+
const key = digestResultCacheIdentity(request.identity);
|
|
20
|
+
const existing = await this.store.read(request.actor, key);
|
|
21
|
+
if (existing && reusable(existing, request, now)) {
|
|
22
|
+
this.emit({ event: 'hit', operationId, reason: 'fresh-compatible-entry', saved: savings(existing.evidence) });
|
|
23
|
+
return { evidence: structuredClone(existing.evidence), receipt: receipt('cache-hit', request.callerInvocationId, existing, existing.evidence, now, false) };
|
|
24
|
+
}
|
|
25
|
+
if (existing) {
|
|
26
|
+
this.emit({ event: 'stale', operationId, reason: 'expired-or-policy-incompatible' });
|
|
27
|
+
if (await this.store.invalidate(request.actor, key, existing.entryId)) {
|
|
28
|
+
this.emit({ event: 'invalidation', operationId, reason: 'expired-or-policy-incompatible' });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
this.emit({ event: 'miss', operationId, reason: 'no-entry' });
|
|
33
|
+
// A flight must never cross an authorization or freshness-policy boundary.
|
|
34
|
+
const flightKey = JSON.stringify([request.actor.tenantId, request.actor.projectId,
|
|
35
|
+
request.actor.workspaceId, request.actor.subjectId, [...request.actor.permissions].sort(),
|
|
36
|
+
key, request.policy.policyVersion, request.policy.sensitivity, request.policy.ttlMs,
|
|
37
|
+
request.policy.negative]);
|
|
38
|
+
const pending = this.flights.get(flightKey);
|
|
39
|
+
if (pending) {
|
|
40
|
+
this.emit({ event: 'single-flight', operationId, reason: 'joined' });
|
|
41
|
+
const completed = await pending;
|
|
42
|
+
if (completed.entry)
|
|
43
|
+
this.emit({ event: 'hit', operationId, reason: 'single-flight-joined', saved: savings(completed.evidence) });
|
|
44
|
+
return { evidence: structuredClone(completed.evidence), receipt: receipt(completed.entry ? 'cache-hit' : 'cache-miss-fill', request.callerInvocationId, completed.entry, completed.evidence, now, false) };
|
|
45
|
+
}
|
|
46
|
+
const work = async () => {
|
|
47
|
+
// Recheck under the cross-process lock; a different worker may have filled
|
|
48
|
+
// while this one waited. Never call the backend merely because the first read missed.
|
|
49
|
+
const published = await this.store.read(request.actor, key);
|
|
50
|
+
if (published && reusable(published, request, now))
|
|
51
|
+
return { evidence: published.evidence, entry: published, reused: true };
|
|
52
|
+
if (published)
|
|
53
|
+
await this.store.invalidate(request.actor, key, published.entryId);
|
|
54
|
+
const evidence = await fill();
|
|
55
|
+
const ttl = cacheableEvidence(evidence, request.policy) && modelEvidenceApproved(request.identity, evidence) ? (evidence.status === 'success' ? request.policy.ttlMs : request.policy.negative.ttlMs) : 0;
|
|
56
|
+
if (ttl <= 0)
|
|
57
|
+
return { evidence, entry: null, reused: false };
|
|
58
|
+
const createdAt = Math.max(now, evidence.evaluatedAtEpochMs);
|
|
59
|
+
const unsigned = { schemaVersion: 'decision-result-cache/v1', revision: 1, entryId: randomUUID(), scope: { tenantId: request.actor.tenantId, projectId: request.actor.projectId, workspaceId: request.actor.workspaceId }, keyDigest: key, identityDigest: key, policyVersion: request.policy.policyVersion, sensitivity: request.policy.sensitivity, createdAtEpochMs: createdAt, expiresAtEpochMs: createdAt + ttl, evidence: { ...evidence, resultDigest: digestCachedResult(evidence.result) } };
|
|
60
|
+
const entry = await this.store.putIfAbsent(request.actor, { ...unsigned, integrityDigest: entryIntegrityDigest(unsigned) });
|
|
61
|
+
return { evidence: entry.evidence, entry, reused: false };
|
|
62
|
+
};
|
|
63
|
+
const promise = this.store.withKeyLock ? this.store.withKeyLock(request.actor, key, work) : work();
|
|
64
|
+
this.flights.set(flightKey, promise);
|
|
65
|
+
try {
|
|
66
|
+
const completed = await promise;
|
|
67
|
+
if (completed.reused)
|
|
68
|
+
this.emit({ event: 'hit', operationId, reason: 'cross-process-joined', saved: savings(completed.evidence) });
|
|
69
|
+
return { evidence: structuredClone(completed.evidence), receipt: receipt(completed.reused ? 'cache-hit' : 'cache-miss-fill', request.callerInvocationId, completed.entry, completed.evidence, now, !completed.reused) };
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
this.flights.delete(flightKey);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async bypass(request, fill, now, operationId, reason) { this.emit({ event: 'bypass', operationId, reason }); const evidence = await fill(); return { evidence, receipt: receipt('bypass', request.callerInvocationId, null, evidence, now, true) }; }
|
|
76
|
+
/** A failing telemetry sink must never alter, reject, or mask a cache decision. */
|
|
77
|
+
emit(event) { try {
|
|
78
|
+
this.telemetry(event);
|
|
79
|
+
}
|
|
80
|
+
catch { /* isolated */ } }
|
|
81
|
+
}
|
|
82
|
+
function cacheable(policy) { return policy.enabled && policy.sideEffectFree && policy.ttlMs > 0 && policy.scope === 'workspace' && policy.policyVersion.length > 0; }
|
|
83
|
+
function modelReusable(identity, now) { const p = identity.modelCompatibility; return p.mode === 'pinned' ? p.actualModel === identity.requestedModel : p.alias === identity.requestedModel && p.snapshotId.length > 0 && p.validUntilEpochMs > now && p.approvedActualModels.length > 0; }
|
|
84
|
+
function modelEvidenceApproved(identity, evidence) { const p = identity.modelCompatibility; return p.mode === 'pinned' ? evidence.actualModel === p.actualModel : p.approvedActualModels.includes(evidence.actualModel); }
|
|
85
|
+
function reusable(entry, request, now) { return entry.expiresAtEpochMs > now && entry.policyVersion === request.policy.policyVersion && entry.sensitivity === request.policy.sensitivity && entry.identityDigest === digestResultCacheIdentity(request.identity) && modelEvidenceApproved(request.identity, entry.evidence); }
|
|
86
|
+
function cacheableEvidence(e, policy) { return e.status === 'success' ? e.failureReason === 'none' : Boolean(policy.negative?.enabled && policy.negative.ttlMs > 0 && policy.negative.ttlMs <= policy.ttlMs && policy.negative.reasons.includes('invalid-input') && e.failureReason === 'invalid-input'); }
|
|
87
|
+
function savings(e) {
|
|
88
|
+
return { inputTokens: e.usage.inputTokens, outputTokens: e.usage.outputTokens,
|
|
89
|
+
costUsd: e.usage.costUsd, latencyMs: e.durationMs, estimated: true };
|
|
90
|
+
}
|
|
91
|
+
function receipt(disposition, caller, entry, evidence, now, providerAttempted) { return { disposition, callerInvocationId: caller, sourceInvocationId: evidence.sourceInvocationId, sourceReceiptId: evidence.sourceReceiptId, originalEvaluatedAtEpochMs: evidence.evaluatedAtEpochMs, cacheEntryId: entry?.entryId ?? null, createdAtEpochMs: now, providerAttempted }; }
|
|
92
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { link, mkdir, open, readFile, readdir, rm, stat, unlink } from 'node:fs/promises';
|
|
3
|
+
import { hostname } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
6
|
+
import { validateDecisionLifecyclePolicy } from '../lifecycle.js';
|
|
7
|
+
import { assertResultCacheEntry } from './integrity.js';
|
|
8
|
+
export class ResultCacheAccessDeniedError extends Error {
|
|
9
|
+
constructor() { super('Result cache operation denied'); }
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Scope components are joined with NUL into the storage key. A component that
|
|
13
|
+
* contains NUL (or any control character) could re-split into another scope, so
|
|
14
|
+
* it is refused before any lookup with the same indistinguishable denial.
|
|
15
|
+
*/
|
|
16
|
+
const SCOPE_COMPONENT = /^[^\u0000-\u001f\u007f]{1,256}$/;
|
|
17
|
+
function validScope(scope) {
|
|
18
|
+
return [scope.tenantId, scope.projectId, scope.workspaceId].every(value => typeof value === 'string' && SCOPE_COMPONENT.test(value));
|
|
19
|
+
}
|
|
20
|
+
function authorize(actor, permission, scope) {
|
|
21
|
+
if (!validScope(actor) || !actor.permissions.includes(permission) || (scope && (actor.tenantId !== scope.tenantId || actor.projectId !== scope.projectId || actor.workspaceId !== scope.workspaceId))) {
|
|
22
|
+
// Deliberately identical for absence, wrong scope, and missing permission.
|
|
23
|
+
throw new ResultCacheAccessDeniedError();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function mapKey(actor, key) { return `${actor.tenantId}\0${actor.projectId}\0${actor.workspaceId}\0${key}`; }
|
|
27
|
+
export class MemoryResultCacheStore {
|
|
28
|
+
entries = new Map();
|
|
29
|
+
async read(actor, key) {
|
|
30
|
+
authorize(actor, 'read');
|
|
31
|
+
const value = this.entries.get(mapKey(actor, key));
|
|
32
|
+
if (!value)
|
|
33
|
+
return null;
|
|
34
|
+
authorize(actor, 'read', value.scope);
|
|
35
|
+
assertResultCacheEntry(value);
|
|
36
|
+
return structuredClone(value);
|
|
37
|
+
}
|
|
38
|
+
async putIfAbsent(actor, entry) {
|
|
39
|
+
authorize(actor, 'write', entry.scope);
|
|
40
|
+
assertResultCacheEntry(entry);
|
|
41
|
+
const key = mapKey(actor, entry.keyDigest);
|
|
42
|
+
const existing = this.entries.get(key);
|
|
43
|
+
if (existing) {
|
|
44
|
+
assertResultCacheEntry(existing);
|
|
45
|
+
return structuredClone(existing);
|
|
46
|
+
}
|
|
47
|
+
this.entries.set(key, structuredClone(entry));
|
|
48
|
+
return structuredClone(entry);
|
|
49
|
+
}
|
|
50
|
+
async invalidate(actor, key, expectedEntryId) { authorize(actor, 'invalidate'); const map = mapKey(actor, key); const current = this.entries.get(map); if (expectedEntryId && current?.entryId !== expectedEntryId)
|
|
51
|
+
return false; return this.entries.delete(map); }
|
|
52
|
+
async delete(actor, key) { authorize(actor, 'delete'); return this.entries.delete(mapKey(actor, key)); }
|
|
53
|
+
async export(actor, key) { authorize(actor, 'export'); const value = this.entries.get(mapKey(actor, key)); if (!value)
|
|
54
|
+
return null; authorize(actor, 'export', value.scope); assertResultCacheEntry(value); return structuredClone(value); }
|
|
55
|
+
}
|
|
56
|
+
const SENSITIVITY_RANK = { public: 0, internal: 1, confidential: 2, restricted: 3 };
|
|
57
|
+
/** Single-file-per-key store. Atomic hard-link publication prevents overwrite/poisoning. */
|
|
58
|
+
export class FileResultCacheStore {
|
|
59
|
+
directory;
|
|
60
|
+
options;
|
|
61
|
+
rule;
|
|
62
|
+
constructor(directory, options = {}) {
|
|
63
|
+
this.directory = directory;
|
|
64
|
+
this.options = options;
|
|
65
|
+
if (options.lifecycle)
|
|
66
|
+
validateDecisionLifecyclePolicy(options.lifecycle.policy);
|
|
67
|
+
this.rule = options.lifecycle ? structuredClone(options.lifecycle.policy.surfaces.cache) : null;
|
|
68
|
+
}
|
|
69
|
+
/** Opaque D10 references (entry IDs) for cache entries derived from one source receipt. */
|
|
70
|
+
async lifecycleReferences(actor, sourceReceiptId) {
|
|
71
|
+
authorize(actor, 'delete');
|
|
72
|
+
return (await this.scopedEntries(actor)).filter(entry => entry.evidence.sourceReceiptId === sourceReceiptId)
|
|
73
|
+
.map(entry => ({ surface: 'cache', opaqueId: entry.entryId }));
|
|
74
|
+
}
|
|
75
|
+
/** D10 `erase` for the `cache` surface: tombstone and unlink the entry with this opaque ID. */
|
|
76
|
+
async eraseLifecycleReference(actor, reference) {
|
|
77
|
+
authorize(actor, 'delete');
|
|
78
|
+
if (reference.surface !== 'cache' || !reference.opaqueId)
|
|
79
|
+
throw new Error('Result cache lifecycle reference invalid');
|
|
80
|
+
const entry = (await this.scopedEntries(actor)).find(value => value.entryId === reference.opaqueId);
|
|
81
|
+
return entry ? this.delete(actor, entry.keyDigest) : false;
|
|
82
|
+
}
|
|
83
|
+
/** Cascade a deleted source receipt to every cache entry in the authenticated scope that reused it. */
|
|
84
|
+
async eraseBySourceReceipt(actor, sourceReceiptId) {
|
|
85
|
+
const references = await this.lifecycleReferences(actor, sourceReceiptId);
|
|
86
|
+
for (const reference of references)
|
|
87
|
+
await this.eraseLifecycleReference(actor, reference);
|
|
88
|
+
return references;
|
|
89
|
+
}
|
|
90
|
+
async withKeyLock(actor, key, work) {
|
|
91
|
+
authorize(actor, 'read');
|
|
92
|
+
authorize(actor, 'write');
|
|
93
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
94
|
+
const lock = `${this.path(actor, key)}.lock`;
|
|
95
|
+
const deadline = Date.now() + 60_000;
|
|
96
|
+
while (true) {
|
|
97
|
+
try {
|
|
98
|
+
const handle = await open(lock, 'wx', 0o600);
|
|
99
|
+
try {
|
|
100
|
+
try {
|
|
101
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, host: hostname() }));
|
|
102
|
+
await handle.sync();
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await handle.close();
|
|
106
|
+
}
|
|
107
|
+
return await work();
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
await rm(lock, { force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code !== 'EEXIST')
|
|
115
|
+
throw error;
|
|
116
|
+
// Only reclaim a dead process on this host; an unreadable or remote lock
|
|
117
|
+
// fails closed rather than risking two provider calls on a shared volume.
|
|
118
|
+
try {
|
|
119
|
+
const before = await stat(lock);
|
|
120
|
+
const owner = JSON.parse(await readFile(lock, 'utf8'));
|
|
121
|
+
if (owner.host === hostname() && Number.isSafeInteger(owner.pid) && owner.pid > 0) {
|
|
122
|
+
let alive = true;
|
|
123
|
+
try {
|
|
124
|
+
process.kill(owner.pid, 0);
|
|
125
|
+
}
|
|
126
|
+
catch (failure) {
|
|
127
|
+
alive = failure.code !== 'ESRCH';
|
|
128
|
+
}
|
|
129
|
+
if (!alive && (await stat(lock)).ino === before.ino) {
|
|
130
|
+
await rm(lock, { force: true });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (failure) {
|
|
136
|
+
// Lock creation and owner write are separate; a transient partial owner
|
|
137
|
+
// is not proof of abandonment. Continue waiting, never break the lock.
|
|
138
|
+
if (failure.code !== 'ENOENT' && !(failure instanceof SyntaxError))
|
|
139
|
+
throw failure;
|
|
140
|
+
}
|
|
141
|
+
if (Date.now() >= deadline)
|
|
142
|
+
throw new Error('Result cache fill lock unavailable');
|
|
143
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Bounded retention sweep for the authenticated scope; never touches other projects. */
|
|
148
|
+
async purgeExpired(actor, nowEpochMs = Date.now()) {
|
|
149
|
+
authorize(actor, 'invalidate');
|
|
150
|
+
let removed = 0;
|
|
151
|
+
for (const entry of await this.scopedEntries(actor)) {
|
|
152
|
+
const expired = entry.expiresAtEpochMs <= nowEpochMs || !this.withinRetention(entry, nowEpochMs)
|
|
153
|
+
|| await this.sourceErased(entry);
|
|
154
|
+
if (expired && await this.invalidate(actor, entry.keyDigest, entry.entryId))
|
|
155
|
+
removed += 1;
|
|
156
|
+
}
|
|
157
|
+
return removed;
|
|
158
|
+
}
|
|
159
|
+
async read(actor, key) { authorize(actor, 'read'); return this.load(actor, key, 'read'); }
|
|
160
|
+
async putIfAbsent(actor, entry) {
|
|
161
|
+
authorize(actor, 'write', entry.scope);
|
|
162
|
+
assertResultCacheEntry(entry);
|
|
163
|
+
// This store writes plaintext JSON. Refuse protected classes until the host
|
|
164
|
+
// provides a separately qualified encrypted storage implementation.
|
|
165
|
+
if (entry.sensitivity === 'confidential' || entry.sensitivity === 'restricted')
|
|
166
|
+
throw new ResultCacheAccessDeniedError();
|
|
167
|
+
// The D10 rule is a ceiling: no entry may outlive the surface retention or exceed its classification.
|
|
168
|
+
if (this.rule && (entry.expiresAtEpochMs - entry.createdAtEpochMs > this.rule.retentionMs
|
|
169
|
+
|| SENSITIVITY_RANK[entry.sensitivity] > SENSITIVITY_RANK[this.rule.classification]))
|
|
170
|
+
throw new ResultCacheAccessDeniedError();
|
|
171
|
+
if (await this.sourceErased(entry))
|
|
172
|
+
throw new ResultCacheAccessDeniedError();
|
|
173
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
174
|
+
const path = this.path(actor, entry.keyDigest);
|
|
175
|
+
if (await this.deleted(actor, entry.keyDigest))
|
|
176
|
+
throw new ResultCacheAccessDeniedError();
|
|
177
|
+
// An entry hidden by D10 retention or source erasure must not block its own replacement.
|
|
178
|
+
const hidden = await this.load(actor, entry.keyDigest, 'write', false);
|
|
179
|
+
if (hidden && (!this.withinRetention(hidden, this.now()) || await this.sourceErased(hidden)))
|
|
180
|
+
await this.remove(actor, entry.keyDigest);
|
|
181
|
+
const tmp = join(this.directory, `.cache-${randomUUID()}.tmp`);
|
|
182
|
+
const handle = await open(tmp, 'wx', 0o600);
|
|
183
|
+
try {
|
|
184
|
+
try {
|
|
185
|
+
await handle.writeFile(`${canonicalJson(entry)}\n`);
|
|
186
|
+
await handle.sync();
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
await handle.close();
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
await link(tmp, path);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error.code !== 'EEXIST')
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await rm(tmp, { force: true });
|
|
201
|
+
}
|
|
202
|
+
if (await this.deleted(actor, entry.keyDigest)) {
|
|
203
|
+
await rm(path, { force: true });
|
|
204
|
+
throw new ResultCacheAccessDeniedError();
|
|
205
|
+
}
|
|
206
|
+
const stored = await this.load(actor, entry.keyDigest, 'write');
|
|
207
|
+
if (!stored)
|
|
208
|
+
throw new Error('Cache publication failed');
|
|
209
|
+
return stored;
|
|
210
|
+
}
|
|
211
|
+
async invalidate(actor, key, expectedEntryId) { authorize(actor, 'invalidate'); if (expectedEntryId) {
|
|
212
|
+
const current = await this.load(actor, key, 'invalidate', false);
|
|
213
|
+
if (!current || current.entryId !== expectedEntryId)
|
|
214
|
+
return false;
|
|
215
|
+
} return this.remove(actor, key); }
|
|
216
|
+
async delete(actor, key) {
|
|
217
|
+
authorize(actor, 'delete');
|
|
218
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
219
|
+
const marker = `${this.path(actor, key)}.deleted`;
|
|
220
|
+
try {
|
|
221
|
+
const handle = await open(marker, 'wx', 0o600);
|
|
222
|
+
try {
|
|
223
|
+
await handle.writeFile(JSON.stringify({ schemaVersion: 'decision-result-cache-tombstone/v1',
|
|
224
|
+
deletedAtEpochMs: Date.now(), legalHold: this.options.legalHold === true }));
|
|
225
|
+
await handle.sync();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await handle.close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code !== 'EEXIST')
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
return this.remove(actor, key);
|
|
236
|
+
}
|
|
237
|
+
async export(actor, key) {
|
|
238
|
+
authorize(actor, 'export');
|
|
239
|
+
// Denied regardless of presence, so the export policy is not an existence oracle.
|
|
240
|
+
if (this.rule?.export === 'denied')
|
|
241
|
+
throw new ResultCacheAccessDeniedError();
|
|
242
|
+
return this.load(actor, key, 'export');
|
|
243
|
+
}
|
|
244
|
+
async load(actor, key, permission, lifecycle = true) {
|
|
245
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
246
|
+
if (await this.deleted(actor, key))
|
|
247
|
+
return null;
|
|
248
|
+
try {
|
|
249
|
+
const entry = JSON.parse(await readFile(this.path(actor, key), 'utf8'));
|
|
250
|
+
authorize(actor, permission, entry.scope);
|
|
251
|
+
assertResultCacheEntry(entry);
|
|
252
|
+
if (entry.sensitivity === 'confidential' || entry.sensitivity === 'restricted')
|
|
253
|
+
throw new ResultCacheAccessDeniedError();
|
|
254
|
+
if (entry.keyDigest !== key)
|
|
255
|
+
throw new Error('Cache key substitution');
|
|
256
|
+
// Beyond D10 retention, or derived from an erased source receipt: treated as absent.
|
|
257
|
+
if (lifecycle && (!this.withinRetention(entry, this.now()) || await this.sourceErased(entry)))
|
|
258
|
+
return null;
|
|
259
|
+
return entry;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (error.code === 'ENOENT')
|
|
263
|
+
return null;
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
now() { return this.options.lifecycle?.now?.() ?? Date.now(); }
|
|
268
|
+
withinRetention(entry, now) { return !this.rule || now - entry.createdAtEpochMs < this.rule.retentionMs; }
|
|
269
|
+
async sourceErased(entry) {
|
|
270
|
+
const erased = this.options.lifecycle?.sourceErased;
|
|
271
|
+
if (!erased)
|
|
272
|
+
return false;
|
|
273
|
+
try {
|
|
274
|
+
return await erased(structuredClone(entry.scope), entry.evidence.sourceReceiptId);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
throw new ResultCacheAccessDeniedError();
|
|
278
|
+
} // An unavailable D10 authority fails closed.
|
|
279
|
+
}
|
|
280
|
+
/** Integrity-checked live entries of the authenticated scope only. */
|
|
281
|
+
async scopedEntries(actor) {
|
|
282
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
283
|
+
const entries = [];
|
|
284
|
+
for (const name of await readdir(this.directory)) {
|
|
285
|
+
if (!/^[a-f0-9]{64}\.json$/.test(name))
|
|
286
|
+
continue;
|
|
287
|
+
const path = join(this.directory, name);
|
|
288
|
+
try {
|
|
289
|
+
const entry = JSON.parse(await readFile(path, 'utf8'));
|
|
290
|
+
if (entry.scope.tenantId !== actor.tenantId || entry.scope.projectId !== actor.projectId
|
|
291
|
+
|| entry.scope.workspaceId !== actor.workspaceId)
|
|
292
|
+
continue;
|
|
293
|
+
assertResultCacheEntry(entry);
|
|
294
|
+
if (this.path(actor, entry.keyDigest) !== path)
|
|
295
|
+
throw new Error('Cache key substitution');
|
|
296
|
+
entries.push(entry);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (error.code !== 'ENOENT')
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return entries;
|
|
304
|
+
}
|
|
305
|
+
async deleted(actor, key) {
|
|
306
|
+
try {
|
|
307
|
+
await stat(`${this.path(actor, key)}.deleted`);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (error.code === 'ENOENT')
|
|
312
|
+
return false;
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async remove(actor, key) { try {
|
|
317
|
+
await unlink(this.path(actor, key));
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (error.code === 'ENOENT')
|
|
322
|
+
return false;
|
|
323
|
+
throw error;
|
|
324
|
+
} }
|
|
325
|
+
path(actor, key) { return join(this.directory, `${createHash('sha256').update(mapKey(actor, key)).digest('hex')}.json`); }
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { reviewDigest } from './validate.js';
|
|
2
|
+
/** Explicit offline/host adapter; no credential handling and no default allow on missing authority. */
|
|
3
|
+
export class PinnedReviewAuthorization {
|
|
4
|
+
authority;
|
|
5
|
+
actionAllowed;
|
|
6
|
+
policyDigest;
|
|
7
|
+
policy;
|
|
8
|
+
constructor(policy, authority, actionAllowed) {
|
|
9
|
+
this.authority = authority;
|
|
10
|
+
this.actionAllowed = actionAllowed;
|
|
11
|
+
if (!policy.id || !policy.version || !policy.tenantId || !policy.projectId ||
|
|
12
|
+
Object.values(policy.minimumQuorumByRisk).some(value => !Number.isSafeInteger(value) || value < 1 || value > 16) ||
|
|
13
|
+
!policy.retentionWindowMsByRisk ||
|
|
14
|
+
(policy.sensitiveViewRoles !== undefined && (!Array.isArray(policy.sensitiveViewRoles) ||
|
|
15
|
+
policy.sensitiveViewRoles.some(role => typeof role !== 'string' || !role))) ||
|
|
16
|
+
Object.values(policy.retentionWindowMsByRisk).some(value => !Number.isSafeInteger(value) || value < 1)) {
|
|
17
|
+
throw new Error('Invalid pinned review policy');
|
|
18
|
+
}
|
|
19
|
+
this.policy = structuredClone(policy);
|
|
20
|
+
this.policyDigest = reviewDigest(this.policy);
|
|
21
|
+
}
|
|
22
|
+
inScope(scope) {
|
|
23
|
+
return scope.tenantId === this.policy.tenantId && scope.projectId === this.policy.projectId;
|
|
24
|
+
}
|
|
25
|
+
async actor(scope, actor) {
|
|
26
|
+
if (!this.inScope(scope) || (actor.id === scope.actor.id && !await this.authority.authenticate(scope)))
|
|
27
|
+
return null;
|
|
28
|
+
const current = await this.authority.resolve(scope.tenantId, scope.projectId, actor.id);
|
|
29
|
+
if (!current?.active || current.compromised)
|
|
30
|
+
return null;
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
hasRole(actual, required) { return actual.some(role => required.includes(role)); }
|
|
34
|
+
async pinned(review) {
|
|
35
|
+
const retention = this.policy.retentionWindowMsByRisk[review.riskTier];
|
|
36
|
+
if (review.tenantId !== this.policy.tenantId || review.projectId !== this.policy.projectId ||
|
|
37
|
+
review.quorum < (this.policy.minimumQuorumByRisk[review.riskTier] ?? Infinity) ||
|
|
38
|
+
retention === undefined || review.retentionUntilEpochMs !== review.createdAtEpochMs + retention)
|
|
39
|
+
return false;
|
|
40
|
+
const pin = review.policyPins.find(item => item.id === this.policy.id && item.version === this.policy.version && item.digest === this.policyDigest);
|
|
41
|
+
return Boolean(pin) && await this.authority.currentPolicyDigest(review.tenantId, review.projectId, this.policy.id) === this.policyDigest;
|
|
42
|
+
}
|
|
43
|
+
async authorize(scope, operation, review) {
|
|
44
|
+
const actor = await this.actor(scope, scope.actor);
|
|
45
|
+
if (!actor || (review && !await this.pinned(review)))
|
|
46
|
+
return false;
|
|
47
|
+
if (!review && operation === 'create')
|
|
48
|
+
return this.hasRole(actor.roles, this.policy.requesterRoles);
|
|
49
|
+
if (!review && operation === 'list')
|
|
50
|
+
return this.hasRole(actor.roles, [
|
|
51
|
+
...this.policy.reviewerRoles, ...this.policy.auditorRoles, ...this.policy.operatorRoles,
|
|
52
|
+
]);
|
|
53
|
+
if (!review && operation === 'purge')
|
|
54
|
+
return this.hasRole(actor.roles, this.policy.operatorRoles);
|
|
55
|
+
if (!review)
|
|
56
|
+
return false;
|
|
57
|
+
switch (operation) {
|
|
58
|
+
case 'create': return this.hasRole(actor.roles, this.policy.requesterRoles);
|
|
59
|
+
case 'list':
|
|
60
|
+
case 'read':
|
|
61
|
+
case 'export': return this.hasRole(actor.roles, [
|
|
62
|
+
...this.policy.reviewerRoles, ...this.policy.auditorRoles, ...this.policy.operatorRoles,
|
|
63
|
+
]);
|
|
64
|
+
case 'resume': return this.hasRole(actor.roles, this.policy.executorRoles);
|
|
65
|
+
case 'sensitive-view': return this.hasRole(actor.roles, this.policy.sensitiveViewRoles ?? []);
|
|
66
|
+
case 'claim':
|
|
67
|
+
case 'decide':
|
|
68
|
+
case 'edit': return this.hasRole(actor.roles, this.policy.reviewerRoles);
|
|
69
|
+
case 'escalate':
|
|
70
|
+
case 'cancel':
|
|
71
|
+
case 'legal-hold':
|
|
72
|
+
case 'delete':
|
|
73
|
+
case 'tombstone':
|
|
74
|
+
case 'purge':
|
|
75
|
+
return this.hasRole(actor.roles, this.policy.operatorRoles);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async eligible(scope, review, proposal) {
|
|
79
|
+
if (!await this.pinned(review) || proposal.version !== review.proposals.at(-1)?.version)
|
|
80
|
+
return false;
|
|
81
|
+
const actor = await this.actor(scope, scope.actor);
|
|
82
|
+
if (!actor || !this.hasRole(actor.roles, this.policy.reviewerRoles.concat(this.policy.executorRoles)))
|
|
83
|
+
return false;
|
|
84
|
+
if (actor.conflictsWith.includes(review.requesterId) || actor.conflictsWith.includes(proposal.editor.id))
|
|
85
|
+
return false;
|
|
86
|
+
if (this.policy.separateRequesterReviewer && scope.actor.id === review.requesterId)
|
|
87
|
+
return false;
|
|
88
|
+
if (this.policy.separateEditorReviewer && scope.actor.id === proposal.editor.id)
|
|
89
|
+
return false;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
async eligibleApproval(scope, review, proposal, decision) {
|
|
93
|
+
if (!await this.pinned(review) || decision.proposalVersion !== proposal.version)
|
|
94
|
+
return false;
|
|
95
|
+
const reviewer = await this.actor(scope, decision.reviewer);
|
|
96
|
+
if (!reviewer || !this.hasRole(reviewer.roles, this.policy.reviewerRoles))
|
|
97
|
+
return false;
|
|
98
|
+
if (this.policy.separateRequesterReviewer && decision.reviewer.id === review.requesterId)
|
|
99
|
+
return false;
|
|
100
|
+
if (this.policy.separateEditorReviewer && decision.reviewer.id === proposal.editor.id)
|
|
101
|
+
return false;
|
|
102
|
+
if (this.policy.separateReviewerExecutor && decision.reviewer.id === scope.actor.id)
|
|
103
|
+
return false;
|
|
104
|
+
return !reviewer.conflictsWith.includes(review.requesterId) && !reviewer.conflictsWith.includes(proposal.editor.id);
|
|
105
|
+
}
|
|
106
|
+
async authorizeAction(scope, review, proposal) {
|
|
107
|
+
return await this.pinned(review) && Boolean(await this.actor(scope, scope.actor)) && await this.actionAllowed(scope, review, proposal);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=authorization.js.map
|