forgeos 0.1.0-alpha.63 → 0.1.0-alpha.64

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.
Files changed (63) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/docs/agent-fabric.md +252 -0
  4. package/package.json +7 -4
  5. package/schemas/agent-fabric/v0.1/control-event.schema.json +281 -0
  6. package/scripts/field-test-forgeos.mjs +11 -2
  7. package/src/forge/_generated/releaseManifest.json +1 -1
  8. package/src/forge/_generated/releaseManifest.ts +3 -3
  9. package/src/forge/agent-adapters/index.ts +18 -14
  10. package/src/forge/agent-adapters/types.ts +1 -1
  11. package/src/forge/agent-fabric/adapter.ts +131 -0
  12. package/src/forge/agent-fabric/authority.ts +298 -0
  13. package/src/forge/agent-fabric/canonical.ts +135 -0
  14. package/src/forge/agent-fabric/conductor.ts +754 -0
  15. package/src/forge/agent-fabric/dictionary.ts +12 -0
  16. package/src/forge/agent-fabric/errors.ts +32 -0
  17. package/src/forge/agent-fabric/hardened-conductor.ts +485 -0
  18. package/src/forge/agent-fabric/hardened-reducer.ts +211 -0
  19. package/src/forge/agent-fabric/index.ts +18 -0
  20. package/src/forge/agent-fabric/journal.ts +116 -0
  21. package/src/forge/agent-fabric/local-approval-window.ts +169 -0
  22. package/src/forge/agent-fabric/local-coding-worker.ts +190 -0
  23. package/src/forge/agent-fabric/local-control-store.ts +239 -0
  24. package/src/forge/agent-fabric/local-paths.ts +18 -0
  25. package/src/forge/agent-fabric/local-task-contract.ts +142 -0
  26. package/src/forge/agent-fabric/local-task-inbox.ts +199 -0
  27. package/src/forge/agent-fabric/local-task-server.ts +207 -0
  28. package/src/forge/agent-fabric/local-task-service.ts +494 -0
  29. package/src/forge/agent-fabric/p0a.ts +104 -0
  30. package/src/forge/agent-fabric/p0b-model-adapter.ts +404 -0
  31. package/src/forge/agent-fabric/planning.ts +155 -0
  32. package/src/forge/agent-fabric/reducer.ts +863 -0
  33. package/src/forge/agent-fabric/resource-ledger.ts +266 -0
  34. package/src/forge/agent-fabric/serialized-local-adapter.ts +60 -0
  35. package/src/forge/agent-fabric/types.ts +424 -0
  36. package/src/forge/agent-fabric/validation.ts +500 -0
  37. package/src/forge/agent-memory/bridge.ts +82 -52
  38. package/src/forge/agent-memory/mcp.ts +59 -9
  39. package/src/forge/agent-memory/sources/codex-hook-runner.mjs +12 -113
  40. package/src/forge/cli/commands.ts +4 -0
  41. package/src/forge/cli/fabric.ts +83 -0
  42. package/src/forge/cli/main.ts +7 -0
  43. package/src/forge/cli/new.ts +16 -4
  44. package/src/forge/cli/parse.ts +21 -0
  45. package/src/forge/cli/verify.ts +4 -1
  46. package/src/forge/compiler/app-graph/tsconfig-hash.ts +41 -4
  47. package/src/forge/compiler/package-graph/dts-extractor.ts +14 -1
  48. package/src/forge/impact/index.ts +18 -21
  49. package/src/forge/runtime/ai/providers.ts +19 -4
  50. package/src/forge/version.ts +1 -1
  51. package/templates/agent-workroom/package.json +1 -0
  52. package/templates/agent-workroom/pnpm-workspace.yaml +2 -0
  53. package/templates/b2b-support-web/package.json +1 -0
  54. package/templates/b2b-support-web/pnpm-workspace.yaml +2 -0
  55. package/templates/b2b-support-web/web/package.json +1 -1
  56. package/templates/minimal-web/package.json +1 -0
  57. package/templates/minimal-web/pnpm-workspace.yaml +2 -0
  58. package/templates/nuxt-web/.yarnrc.yml +1 -0
  59. package/templates/nuxt-web/package.json +1 -0
  60. package/templates/nuxt-web/pnpm-workspace.yaml +2 -0
  61. package/templates/nuxt-web/web/package.json +1 -0
  62. package/templates/vendor-access/package.json +1 -0
  63. package/templates/vendor-access/pnpm-workspace.yaml +2 -0
@@ -66,7 +66,16 @@ function normalizeForgeSpec(spec) {
66
66
  }
67
67
 
68
68
  function commandName(command) {
69
- return process.platform === "win32" ? `${command}.cmd` : command;
69
+ if (process.platform !== "win32") return command;
70
+ return command === "bun" ? command : `${command}.cmd`;
71
+ }
72
+
73
+ function fieldScratchRoot() {
74
+ const runnerTemp = process.env.RUNNER_TEMP;
75
+ if (process.platform === "win32" && runnerTemp && isAbsolute(runnerTemp)) {
76
+ return runnerTemp;
77
+ }
78
+ return tmpdir();
70
79
  }
71
80
 
72
81
  function commandLine(command, args) {
@@ -944,7 +953,7 @@ async function main() {
944
953
  return;
945
954
  }
946
955
 
947
- const appRoot = await mkdtemp(join(tmpdir(), "forgeos-field-"));
956
+ const appRoot = await mkdtemp(join(fieldScratchRoot(), "forgeos-field-"));
948
957
  const results = [];
949
958
  try {
950
959
  for (const testCase of cases) {
@@ -1 +1 @@
1
- {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.63","releaseId":"forgeos@0.1.0-alpha.63+unknown","schemaVersion":"0.1.0"}
1
+ {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.64","releaseId":"forgeos@0.1.0-alpha.64+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.63 input=6671cc4ce9f1aeab62768f037489379d8303a79d2681b1fae4cb42a84e818b30 content=407b2f82bfffde0fd6fec96fc9130364878d1f2a6d0945e40da2b05d49aea75a
1
+ // @forge-generated generator=0.1.0-alpha.64 input=5bea3de626e4a7db3a5bbe226a820619b19a6e1347922b0a4f97fedf6654c489 content=805acd8e59d13986e509830825226246b9aa99c68f254c7879db5648cec66b9f
2
2
  export const releaseManifest = {
3
3
  "defaultProvider": "local",
4
4
  "diagnostics": [],
@@ -19,7 +19,7 @@ export const releaseManifest = {
19
19
  "custom"
20
20
  ],
21
21
  "packageName": "forgeos",
22
- "packageVersion": "0.1.0-alpha.63",
23
- "releaseId": "forgeos@0.1.0-alpha.63+unknown",
22
+ "packageVersion": "0.1.0-alpha.64",
23
+ "releaseId": "forgeos@0.1.0-alpha.64+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -998,6 +998,8 @@ function eventIsNativeHookEvent(event: AgentMemoryEventRecord): boolean {
998
998
  return (
999
999
  !eventIsForgeHookCanary(event) &&
1000
1000
  !eventIsForgeHookProbe(event) &&
1001
+ typeof event.externalSessionId === "string" &&
1002
+ event.externalSessionId !== "forge-hook-probe" &&
1001
1003
  event.integrationKind === "native-hook" &&
1002
1004
  event.trustLevel === "direct-hook"
1003
1005
  );
@@ -1009,7 +1011,7 @@ function hookApprovalStatusFor(
1009
1011
  nativeSignals: number,
1010
1012
  observableSignals = 0,
1011
1013
  memoryReadable = true,
1012
- ): "not-required" | "waiting-for-user-trust" | "accepted" | "trusted" | "memory-unavailable" {
1014
+ ): "not-required" | "waiting-for-user-trust" | "unverified" | "trusted" | "memory-unavailable" {
1013
1015
  if (target !== "codex" || !installed) {
1014
1016
  return "not-required";
1015
1017
  }
@@ -1019,7 +1021,9 @@ function hookApprovalStatusFor(
1019
1021
  if (nativeSignals > 0) {
1020
1022
  return "trusted";
1021
1023
  }
1022
- return observableSignals > 0 ? "accepted" : "waiting-for-user-trust";
1024
+ // A Forge-generated canary proves only the local ingest path. It cannot
1025
+ // establish that the user trusted the hook definition in Codex Desktop.
1026
+ return observableSignals > 0 ? "unverified" : "waiting-for-user-trust";
1023
1027
  }
1024
1028
 
1025
1029
  function hookNativeTrustStatusFor(
@@ -1044,12 +1048,12 @@ function codexHookApprovalMessage(
1044
1048
  ): string {
1045
1049
  if (approvalStatus === "waiting-for-user-trust") {
1046
1050
  if (canarySignals > 0) {
1047
- return "ForgeOS can see the Codex smoke canary; hook approval is accepted, but trusted native Codex signal proof is still pending";
1051
+ return "ForgeOS can see a smoke canary, but Codex hook approval is unverified; review the hook in Codex Desktop and wait for a native signal";
1048
1052
  }
1049
1053
  return "ForgeOS has not seen a trusted native Codex hook signal yet; approve the Codex Desktop hook prompt if shown, then continue a Codex session in this workspace";
1050
1054
  }
1051
- if (approvalStatus === "accepted" && nativeTrustStatus === "waiting-for-native-signal") {
1052
- return "Codex hook approval is accepted for local editing; ForgeOS is still waiting for a trusted native Codex signal for stronger provenance";
1055
+ if (approvalStatus === "unverified" && nativeTrustStatus === "waiting-for-native-signal") {
1056
+ return "Codex hook approval is unverified; smoke canaries do not prove user trust or native execution";
1053
1057
  }
1054
1058
  if (approvalStatus === "trusted") {
1055
1059
  return "Codex Desktop hook trust is confirmed by a native hook signal";
@@ -1368,7 +1372,7 @@ async function readAgentHookStatus(options: AgentCommandOptions): Promise<AgentH
1368
1372
  const observableSignals = Math.max(usefulSignals, canarySignals);
1369
1373
  const approvalStatus = hookApprovalStatusFor(target, installed, nativeSignals, observableSignals, deltaWritable);
1370
1374
  const nativeTrustStatus = hookNativeTrustStatusFor(target, installed, nativeSignals, deltaWritable);
1371
- const approvalRequired = approvalStatus === "waiting-for-user-trust";
1375
+ const approvalRequired = approvalStatus === "waiting-for-user-trust" || approvalStatus === "unverified";
1372
1376
  const codexHookInspection = installTarget === "codex"
1373
1377
  ? inspectCodexHookCommands(options.workspaceRoot)
1374
1378
  : undefined;
@@ -1481,14 +1485,14 @@ async function readAgentHookStatus(options: AgentCommandOptions): Promise<AgentH
1481
1485
  },
1482
1486
  {
1483
1487
  name: "native-hook-signal",
1484
- ok: target !== "codex" || !deltaWritable || nativeSignals > 0 || approvalStatus === "accepted",
1488
+ ok: target !== "codex" || !deltaWritable || nativeSignals > 0,
1485
1489
  message: target !== "codex"
1486
1490
  ? "native Codex hook approval is not required for this target"
1487
1491
  : !deltaWritable
1488
1492
  ? "trusted Codex native hook signals cannot be verified until Agent Memory is readable"
1489
1493
  : nativeSignals > 0
1490
1494
  ? `${nativeSignals} trusted Codex native hook signal(s) visible or queued`
1491
- : "hook approval is accepted, but Codex has not emitted a trusted native hook signal yet",
1495
+ : "only canary signals are visible; Codex has not emitted a trusted native hook signal",
1492
1496
  evidence: {
1493
1497
  nativeSignals,
1494
1498
  canarySignals,
@@ -1612,7 +1616,7 @@ export async function runAgentDoctor(options: AgentCommandOptions): Promise<Agen
1612
1616
  memoryReadable,
1613
1617
  );
1614
1618
  const nativeTrustStatus = hookNativeTrustStatusFor(target, Boolean(installTarget && installed), nativeSignals, memoryReadable);
1615
- const approvalRequired = approvalStatus === "waiting-for-user-trust";
1619
+ const approvalRequired = approvalStatus === "waiting-for-user-trust" || approvalStatus === "unverified";
1616
1620
  const hookBridgeState = !installTarget
1617
1621
  ? "not-supported"
1618
1622
  : !memoryReadable
@@ -1690,14 +1694,14 @@ export async function runAgentDoctor(options: AgentCommandOptions): Promise<Agen
1690
1694
  },
1691
1695
  {
1692
1696
  name: "native-hook-signal",
1693
- ok: !installTarget || target !== "codex" || !memoryReadable || nativeSignals > 0 || approvalStatus === "accepted",
1697
+ ok: !installTarget || target !== "codex" || !memoryReadable || nativeSignals > 0,
1694
1698
  message: !installTarget || target !== "codex"
1695
1699
  ? "native Codex hook approval is not required for this target"
1696
1700
  : !memoryReadable
1697
1701
  ? "trusted Codex native hook signals cannot be verified until Agent Memory is readable"
1698
1702
  : nativeSignals > 0
1699
1703
  ? `${nativeSignals} trusted Codex native hook signal(s) visible or queued`
1700
- : "hook approval is accepted, but Codex has not emitted a trusted native hook signal yet",
1704
+ : "only canary signals are visible; Codex has not emitted a trusted native hook signal",
1701
1705
  evidence: {
1702
1706
  nativeSignals,
1703
1707
  canarySignals,
@@ -1911,8 +1915,8 @@ export async function runAgentOnboard(options: AgentCommandOptions): Promise<Age
1911
1915
  ? "Codex hook trust cannot be verified until Agent Memory is readable"
1912
1916
  : doctor.summary.approvalStatus === "trusted"
1913
1917
  ? "Codex hook trust is confirmed by a native hook signal"
1914
- : doctor.summary.approvalStatus === "accepted"
1915
- ? "Codex hook approval is accepted; trusted native signal proof is still pending"
1918
+ : doctor.summary.approvalStatus === "unverified"
1919
+ ? "Codex hook approval is unverified; a smoke canary is not native proof"
1916
1920
  : "hook approval is not required for this target",
1917
1921
  }]
1918
1922
  : []),
@@ -2171,7 +2175,7 @@ export async function runAgentHooksSmoke(options: AgentCommandOptions): Promise<
2171
2175
  ? [createDiagnostic({
2172
2176
  severity: "warning",
2173
2177
  code: "FORGE_AGENT_HOOK_APPROVAL_REQUIRED",
2174
- message: "Codex Desktop hook approval is still pending because ForgeOS has not seen a canary, useful hook event, or trusted native hook signal yet. Approve the hook prompt if Codex shows one, then continue a Codex session in this workspace.",
2178
+ message: "Codex Desktop hook approval cannot be inferred from a smoke canary. Review the exact hook definition in Codex Desktop, then continue a session and verify a native hook signal.",
2175
2179
  suggestedCommands: hookApprovalNextActions(target, status.canarySignals),
2176
2180
  })]
2177
2181
  : []),
@@ -140,7 +140,7 @@ export interface AgentPrintContextResult {
140
140
  }
141
141
 
142
142
  export type AgentHookBridgeState = "ready" | "missing" | "not-supported" | "waiting-for-user-trust" | "memory-unavailable";
143
- export type AgentHookApprovalStatus = "not-required" | "waiting-for-user-trust" | "accepted" | "trusted" | "memory-unavailable";
143
+ export type AgentHookApprovalStatus = "not-required" | "waiting-for-user-trust" | "unverified" | "trusted" | "memory-unavailable";
144
144
  export type AgentHookNativeTrustStatus = "not-required" | "waiting-for-native-signal" | "trusted" | "memory-unavailable";
145
145
  export type AgentHookReadinessLevel = "none" | "canary" | "trusted-native";
146
146
 
@@ -0,0 +1,131 @@
1
+ import { AgentFabricError } from "./errors.ts";
2
+ import type {
3
+ AdapterManifest,
4
+ AdapterOutcomeResult,
5
+ AdapterStartResult,
6
+ AgentAdapter,
7
+ AttemptExecutionPermit,
8
+ Digest,
9
+ RuntimeObservation,
10
+ WorkerResultReport,
11
+ } from "./types.ts";
12
+
13
+ export interface DeterministicAdapterFixture {
14
+ effectiveRunSpecDigest: Digest;
15
+ outcomeStatus: "succeeded" | "failed";
16
+ resultDigest: Digest;
17
+ evidenceDigests?: readonly Digest[];
18
+ }
19
+
20
+ interface AttemptRecord {
21
+ permit: AttemptExecutionPermit;
22
+ startedAt: number;
23
+ cancelled: boolean;
24
+ }
25
+
26
+ export class DeterministicTestAdapter implements AgentAdapter {
27
+ private readonly fixtures = new Map<Digest, DeterministicAdapterFixture>();
28
+ private readonly attempts = new Map<string, AttemptRecord>();
29
+
30
+ constructor(
31
+ fixtures: readonly DeterministicAdapterFixture[],
32
+ private readonly now: () => number,
33
+ ) {
34
+ for (const fixture of fixtures) this.fixtures.set(fixture.effectiveRunSpecDigest, fixture);
35
+ }
36
+
37
+ manifest(): AdapterManifest {
38
+ return {
39
+ adapterId: "forge-agent-fabric/deterministic-test-adapter",
40
+ version: "0.1.0",
41
+ capabilities: ["deterministic_fixture_execution", "observation", "cancellation"],
42
+ supportsCancellation: true,
43
+ supportsObservation: true,
44
+ };
45
+ }
46
+
47
+ async startAttempt(permit: AttemptExecutionPermit): Promise<AdapterStartResult> {
48
+ const existing = this.attempts.get(permit.attemptId);
49
+ if (existing) {
50
+ if (existing.permit.permitId !== permit.permitId) {
51
+ throw new AgentFabricError(
52
+ "AF_CONFLICT",
53
+ `Attempt ${permit.attemptId} was started with a different permit`,
54
+ );
55
+ }
56
+ return {
57
+ status: "started",
58
+ report: {
59
+ startupReportId: `startup:${permit.attemptId}`,
60
+ attemptId: permit.attemptId,
61
+ observedSpecDigest: permit.effectiveRunSpecDigest,
62
+ startedAt: existing.startedAt,
63
+ },
64
+ };
65
+ }
66
+ if (!this.fixtures.has(permit.effectiveRunSpecDigest)) {
67
+ return { status: "unknown", reason: "fixture_not_found" };
68
+ }
69
+ const startedAt = this.now();
70
+ this.attempts.set(permit.attemptId, { permit, startedAt, cancelled: false });
71
+ return {
72
+ status: "started",
73
+ report: {
74
+ startupReportId: `startup:${permit.attemptId}`,
75
+ attemptId: permit.attemptId,
76
+ observedSpecDigest: permit.effectiveRunSpecDigest,
77
+ startedAt,
78
+ },
79
+ };
80
+ }
81
+
82
+ async observeAttempt(attemptId: string): Promise<readonly RuntimeObservation[]> {
83
+ const attempt = this.attempts.get(attemptId);
84
+ if (!attempt) return [];
85
+ return [
86
+ {
87
+ observationId: `observation:${attemptId}:adapter-state`,
88
+ attemptId,
89
+ sourceClass: "adapter_observation",
90
+ claim: attempt.cancelled ? "cancelled" : "running_or_completed",
91
+ observedAt: this.now(),
92
+ },
93
+ ];
94
+ }
95
+
96
+ async collectOutcome(attemptId: string): Promise<AdapterOutcomeResult> {
97
+ const attempt = this.attempts.get(attemptId);
98
+ if (!attempt) return { status: "unknown", reason: "attempt_not_observed" };
99
+ if (attempt.cancelled) return { status: "unknown", reason: "attempt_cancelled" };
100
+ const fixture = this.fixtures.get(attempt.permit.effectiveRunSpecDigest);
101
+ if (!fixture) return { status: "unknown", reason: "fixture_not_found" };
102
+ const permit = attempt.permit;
103
+ const report: WorkerResultReport = {
104
+ reportId: `report:${attemptId}`,
105
+ attemptId,
106
+ permitId: permit.permitId,
107
+ intentId: permit.intentId,
108
+ planRevisionId: permit.planRevisionId,
109
+ effectiveRunSpecDigest: permit.effectiveRunSpecDigest,
110
+ fencingToken: permit.fencingToken,
111
+ status: fixture.outcomeStatus,
112
+ resultDigest: fixture.resultDigest,
113
+ evidenceDigests: fixture.evidenceDigests ?? [],
114
+ reportedAt: this.now(),
115
+ };
116
+ return { status: "reported", report };
117
+ }
118
+
119
+ async requestCancellation(attemptId: string): Promise<{ acknowledged: boolean }> {
120
+ const attempt = this.attempts.get(attemptId);
121
+ if (!attempt) return { acknowledged: false };
122
+ attempt.cancelled = true;
123
+ return { acknowledged: true };
124
+ }
125
+
126
+ async observeTermination(attemptId: string): Promise<"terminated" | "running" | "unknown"> {
127
+ const attempt = this.attempts.get(attemptId);
128
+ if (!attempt) return "unknown";
129
+ return attempt.cancelled ? "terminated" : "running";
130
+ }
131
+ }
@@ -0,0 +1,298 @@
1
+ import { getOwn, setOwn } from "./dictionary.ts";
2
+ import { AgentFabricError } from "./errors.ts";
3
+ import { ResourceLedger } from "./resource-ledger.ts";
4
+ import type {
5
+ AuthorityResolution,
6
+ ControlState,
7
+ DerivedGrantRequest,
8
+ EffectClass,
9
+ ExecutionGrant,
10
+ OwnerAuthorization,
11
+ } from "./types.ts";
12
+
13
+ function isSubset<T>(child: readonly T[], parent: readonly T[]): boolean {
14
+ const allowed = new Set(parent);
15
+ return child.every((value) => allowed.has(value));
16
+ }
17
+
18
+ function reject(...reasonCodes: string[]): AuthorityResolution {
19
+ return { outcome: "rejected", reasonCodes, limitations: [] };
20
+ }
21
+
22
+ export function rootGrantAuthorizationViolations(
23
+ authorization: OwnerAuthorization,
24
+ grant: ExecutionGrant,
25
+ ): readonly string[] {
26
+ const violations: string[] = [];
27
+ if (grant.parentGrantId !== null) violations.push("root_grant_has_parent");
28
+ if (grant.rootAuthorizationId !== authorization.authorizationId) {
29
+ violations.push("root_authorization_mismatch");
30
+ }
31
+ if (authorization.rootExecutionId.length === 0) violations.push("authorization_missing_execution");
32
+ if (!authorization.subjectIds.includes(grant.subjectId)) violations.push("subject_not_authorized");
33
+ if (!isSubset(grant.capabilities, authorization.capabilities)) violations.push("capability_scope_expanded");
34
+ if (!isSubset(grant.sourceIds, authorization.sourceIds)) violations.push("source_scope_expanded");
35
+ if (!isSubset(grant.targetIds, authorization.targetIds)) violations.push("target_scope_expanded");
36
+ if (!isSubset<EffectClass>(grant.effectClasses, authorization.effectClasses)) {
37
+ violations.push("effect_scope_expanded");
38
+ }
39
+ if (grant.notBefore < authorization.notBefore || grant.expiresAt > authorization.expiresAt) {
40
+ violations.push("time_scope_expanded");
41
+ }
42
+ if (grant.expiresAt <= grant.notBefore) violations.push("invalid_time_window");
43
+ if (grant.maximumAttempts <= 0 || grant.maximumAttempts > authorization.maximumAttempts) {
44
+ violations.push("attempt_limit_expanded");
45
+ }
46
+ if (
47
+ grant.delegationDepthRemaining < 0 ||
48
+ grant.delegationDepthRemaining > authorization.maximumDelegationDepth
49
+ ) {
50
+ violations.push("delegation_depth_expanded");
51
+ }
52
+ for (const [resource, amount] of Object.entries(grant.resourceCeilings)) {
53
+ const ceiling = getOwn(authorization.resourceCeilings, resource);
54
+ if (!Number.isFinite(amount) || amount <= 0 || ceiling === undefined || amount > ceiling) {
55
+ violations.push(`resource_ceiling_expanded:${resource}`);
56
+ }
57
+ }
58
+ return violations;
59
+ }
60
+
61
+ export function assertRootGrantAuthorized(
62
+ authorization: OwnerAuthorization,
63
+ grant: ExecutionGrant,
64
+ ): void {
65
+ const violations = rootGrantAuthorizationViolations(authorization, grant);
66
+ if (violations.length > 0) {
67
+ throw new AgentFabricError(
68
+ "AF_GRANT_REJECTED",
69
+ `Root grant ${grant.grantId} exceeds authorization ${authorization.authorizationId}`,
70
+ { violations },
71
+ );
72
+ }
73
+ }
74
+
75
+ export function grantAttenuationViolations(
76
+ parent: ExecutionGrant,
77
+ child: ExecutionGrant,
78
+ ): readonly string[] {
79
+ const violations: string[] = [];
80
+ if (child.parentGrantId !== parent.grantId) violations.push("parent_grant_mismatch");
81
+ if (child.rootAuthorizationId !== parent.rootAuthorizationId) {
82
+ violations.push("root_authorization_changed");
83
+ }
84
+ if (!child.reservationId) violations.push("derived_grant_missing_reservation");
85
+ if (!isSubset(child.capabilities, parent.capabilities)) violations.push("capability_scope_expanded");
86
+ if (!isSubset(child.sourceIds, parent.sourceIds)) violations.push("source_scope_expanded");
87
+ if (!isSubset(child.targetIds, parent.targetIds)) violations.push("target_scope_expanded");
88
+ if (!isSubset<EffectClass>(child.effectClasses, parent.effectClasses)) {
89
+ violations.push("effect_scope_expanded");
90
+ }
91
+ if (child.notBefore < parent.notBefore || child.expiresAt > parent.expiresAt) {
92
+ violations.push("time_scope_expanded");
93
+ }
94
+ if (child.expiresAt <= child.notBefore) violations.push("invalid_time_window");
95
+ if (child.maximumAttempts <= 0 || child.maximumAttempts > parent.maximumAttempts) {
96
+ violations.push("attempt_limit_expanded");
97
+ }
98
+ if (
99
+ child.delegationDepthRemaining < 0 ||
100
+ child.delegationDepthRemaining >= parent.delegationDepthRemaining
101
+ ) {
102
+ violations.push("delegation_depth_not_attenuated");
103
+ }
104
+ for (const [resource, amount] of Object.entries(child.resourceCeilings)) {
105
+ const ceiling = getOwn(parent.resourceCeilings, resource);
106
+ if (!Number.isFinite(amount) || amount <= 0 || ceiling === undefined || amount > ceiling) {
107
+ violations.push(`resource_ceiling_expanded:${resource}`);
108
+ }
109
+ }
110
+ return violations;
111
+ }
112
+
113
+ export function assertGrantAttenuated(
114
+ parent: ExecutionGrant,
115
+ child: ExecutionGrant,
116
+ ): void {
117
+ const violations = grantAttenuationViolations(parent, child);
118
+ if (violations.length > 0) {
119
+ throw new AgentFabricError(
120
+ "AF_GRANT_REJECTED",
121
+ `Derived grant ${child.grantId} is not attenuated from ${parent.grantId}`,
122
+ { violations },
123
+ );
124
+ }
125
+ }
126
+
127
+ export function deriveExecutionGrant(
128
+ parent: ExecutionGrant,
129
+ request: DerivedGrantRequest,
130
+ ledger: ResourceLedger,
131
+ now: number,
132
+ ): AuthorityResolution {
133
+ if (now < parent.notBefore || now >= parent.expiresAt) {
134
+ return reject("parent_grant_not_current");
135
+ }
136
+
137
+ const aggregatedResourceRequests = Object.entries(
138
+ request.resourceRequests.reduce<Record<string, number>>((totals, resourceRequest) => {
139
+ setOwn(totals, resourceRequest.resource, (getOwn(totals, resourceRequest.resource) ?? 0) + resourceRequest.amount);
140
+ return totals;
141
+ }, {}),
142
+ ).map(([resource, amount]) => ({ resource, amount }));
143
+
144
+ const candidate: ExecutionGrant = {
145
+ grantId: request.grantId,
146
+ rootAuthorizationId: parent.rootAuthorizationId,
147
+ subjectId: request.subjectId,
148
+ parentGrantId: parent.grantId,
149
+ capabilities: [...request.capabilities],
150
+ sourceIds: [...request.sourceIds],
151
+ targetIds: [...request.targetIds],
152
+ effectClasses: [...request.effectClasses],
153
+ notBefore: request.notBefore,
154
+ expiresAt: request.expiresAt,
155
+ maximumAttempts: request.maximumAttempts,
156
+ delegationDepthRemaining: request.delegationDepthRemaining,
157
+ resourceCeilings: Object.fromEntries(
158
+ aggregatedResourceRequests.map((item) => [item.resource, item.amount]),
159
+ ),
160
+ reservationId: request.reservationId,
161
+ };
162
+ const violations = grantAttenuationViolations(parent, candidate);
163
+ if (violations.length > 0) return reject(...violations);
164
+
165
+ try {
166
+ const reserved = ledger.reserve(
167
+ request.reservationId,
168
+ parent.grantId,
169
+ aggregatedResourceRequests,
170
+ parent.resourceCeilings,
171
+ );
172
+ return {
173
+ outcome: "allowed",
174
+ reasonCodes: [],
175
+ limitations: [],
176
+ grant: {
177
+ ...candidate,
178
+ resourceCeilings: Object.fromEntries(
179
+ reserved.requests.map((item) => [item.resource, item.amount]),
180
+ ),
181
+ },
182
+ };
183
+ } catch (error) {
184
+ if (error instanceof AgentFabricError) {
185
+ return reject(error.code.toLowerCase());
186
+ }
187
+ return { outcome: "unknown", reasonCodes: ["resource_reservation_unknown"], limitations: [] };
188
+ }
189
+ }
190
+
191
+ export function assertAuthorizationCurrent(
192
+ authorization: OwnerAuthorization,
193
+ now: number,
194
+ revokedReason?: string,
195
+ ): void {
196
+ if (revokedReason) {
197
+ throw new AgentFabricError(
198
+ "AF_GRANT_REJECTED",
199
+ `Owner authorization ${authorization.authorizationId} is revoked`,
200
+ { revokedReason },
201
+ );
202
+ }
203
+ if (now < authorization.notBefore || now >= authorization.expiresAt) {
204
+ throw new AgentFabricError(
205
+ "AF_GRANT_REJECTED",
206
+ `Owner authorization ${authorization.authorizationId} is outside its validity window`,
207
+ );
208
+ }
209
+ }
210
+
211
+ export function assertGrantCurrent(
212
+ grant: ExecutionGrant,
213
+ now: number,
214
+ revokedReason?: string,
215
+ ): void {
216
+ if (revokedReason) {
217
+ throw new AgentFabricError(
218
+ "AF_GRANT_REJECTED",
219
+ `Grant ${grant.grantId} is revoked`,
220
+ { revokedReason },
221
+ );
222
+ }
223
+ if (now < grant.notBefore || now >= grant.expiresAt) {
224
+ throw new AgentFabricError(
225
+ "AF_GRANT_REJECTED",
226
+ `Grant ${grant.grantId} is outside its validity window`,
227
+ );
228
+ }
229
+ }
230
+
231
+ export function assertGrantLineageCurrent(
232
+ state: Pick<
233
+ ControlState,
234
+ | "authorizations"
235
+ | "revokedAuthorizations"
236
+ | "grants"
237
+ | "revokedGrants"
238
+ | "resourceReservations"
239
+ >,
240
+ grantId: string,
241
+ now: number,
242
+ ): void {
243
+ const seen = new Set<string>();
244
+ const selected = getOwn(state.grants, grantId);
245
+ if (!selected) throw new AgentFabricError("AF_NOT_FOUND", `Unknown grant: ${grantId}`);
246
+ let current: ExecutionGrant = selected;
247
+
248
+ while (true) {
249
+ if (seen.has(current.grantId)) {
250
+ throw new AgentFabricError("AF_GRANT_REJECTED", "Grant ancestry contains a cycle");
251
+ }
252
+ seen.add(current.grantId);
253
+ assertGrantCurrent(current, now, getOwn(state.revokedGrants, current.grantId));
254
+
255
+ if (current.parentGrantId) {
256
+ if (!current.reservationId) {
257
+ throw new AgentFabricError(
258
+ "AF_GRANT_REJECTED",
259
+ `Derived grant ${current.grantId} has no resource reservation`,
260
+ );
261
+ }
262
+ const reservation = getOwn(state.resourceReservations, current.reservationId);
263
+ if (!reservation || reservation.status === "released") {
264
+ throw new AgentFabricError(
265
+ "AF_GRANT_REJECTED",
266
+ `Derived grant ${current.grantId} has no current resource reservation`,
267
+ );
268
+ }
269
+ }
270
+
271
+ if (!current.parentGrantId) {
272
+ const authorization = getOwn(state.authorizations, current.rootAuthorizationId);
273
+ if (!authorization) {
274
+ throw new AgentFabricError(
275
+ "AF_GRANT_REJECTED",
276
+ `Root grant ${current.grantId} has no registered owner authorization`,
277
+ );
278
+ }
279
+ assertAuthorizationCurrent(
280
+ authorization,
281
+ now,
282
+ getOwn(state.revokedAuthorizations, authorization.authorizationId),
283
+ );
284
+ assertRootGrantAuthorized(authorization, current);
285
+ return;
286
+ }
287
+
288
+ const parent: ExecutionGrant | undefined = getOwn(state.grants, current.parentGrantId);
289
+ if (!parent) {
290
+ throw new AgentFabricError(
291
+ "AF_GRANT_REJECTED",
292
+ `Grant ${current.grantId} references missing parent ${current.parentGrantId}`,
293
+ );
294
+ }
295
+ assertGrantAttenuated(parent, current);
296
+ current = parent;
297
+ }
298
+ }