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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +19 -0
- package/docs/agent-fabric.md +252 -0
- package/package.json +7 -4
- package/schemas/agent-fabric/v0.1/control-event.schema.json +281 -0
- package/scripts/field-test-forgeos.mjs +11 -2
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/index.ts +18 -14
- package/src/forge/agent-adapters/types.ts +1 -1
- package/src/forge/agent-fabric/adapter.ts +131 -0
- package/src/forge/agent-fabric/authority.ts +298 -0
- package/src/forge/agent-fabric/canonical.ts +135 -0
- package/src/forge/agent-fabric/conductor.ts +754 -0
- package/src/forge/agent-fabric/dictionary.ts +12 -0
- package/src/forge/agent-fabric/errors.ts +32 -0
- package/src/forge/agent-fabric/hardened-conductor.ts +485 -0
- package/src/forge/agent-fabric/hardened-reducer.ts +211 -0
- package/src/forge/agent-fabric/index.ts +18 -0
- package/src/forge/agent-fabric/journal.ts +116 -0
- package/src/forge/agent-fabric/local-approval-window.ts +169 -0
- package/src/forge/agent-fabric/local-coding-worker.ts +190 -0
- package/src/forge/agent-fabric/local-control-store.ts +239 -0
- package/src/forge/agent-fabric/local-paths.ts +18 -0
- package/src/forge/agent-fabric/local-task-contract.ts +142 -0
- package/src/forge/agent-fabric/local-task-inbox.ts +199 -0
- package/src/forge/agent-fabric/local-task-server.ts +207 -0
- package/src/forge/agent-fabric/local-task-service.ts +494 -0
- package/src/forge/agent-fabric/p0a.ts +104 -0
- package/src/forge/agent-fabric/p0b-model-adapter.ts +404 -0
- package/src/forge/agent-fabric/planning.ts +155 -0
- package/src/forge/agent-fabric/reducer.ts +863 -0
- package/src/forge/agent-fabric/resource-ledger.ts +266 -0
- package/src/forge/agent-fabric/serialized-local-adapter.ts +60 -0
- package/src/forge/agent-fabric/types.ts +424 -0
- package/src/forge/agent-fabric/validation.ts +500 -0
- package/src/forge/agent-memory/bridge.ts +82 -52
- package/src/forge/agent-memory/mcp.ts +59 -9
- package/src/forge/agent-memory/sources/codex-hook-runner.mjs +12 -113
- package/src/forge/cli/commands.ts +4 -0
- package/src/forge/cli/fabric.ts +83 -0
- package/src/forge/cli/main.ts +7 -0
- package/src/forge/cli/new.ts +16 -4
- package/src/forge/cli/parse.ts +21 -0
- package/src/forge/cli/verify.ts +4 -1
- package/src/forge/compiler/app-graph/tsconfig-hash.ts +41 -4
- package/src/forge/compiler/package-graph/dts-extractor.ts +14 -1
- package/src/forge/impact/index.ts +18 -21
- package/src/forge/runtime/ai/providers.ts +19 -4
- package/src/forge/version.ts +1 -1
- package/templates/agent-workroom/package.json +1 -0
- package/templates/agent-workroom/pnpm-workspace.yaml +2 -0
- package/templates/b2b-support-web/package.json +1 -0
- package/templates/b2b-support-web/pnpm-workspace.yaml +2 -0
- package/templates/b2b-support-web/web/package.json +1 -1
- package/templates/minimal-web/package.json +1 -0
- package/templates/minimal-web/pnpm-workspace.yaml +2 -0
- package/templates/nuxt-web/.yarnrc.yml +1 -0
- package/templates/nuxt-web/package.json +1 -0
- package/templates/nuxt-web/pnpm-workspace.yaml +2 -0
- package/templates/nuxt-web/web/package.json +1 -0
- package/templates/vendor-access/package.json +1 -0
- 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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
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" | "
|
|
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
|
-
|
|
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
|
|
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 === "
|
|
1052
|
-
return "Codex hook approval is
|
|
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
|
|
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
|
-
: "
|
|
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
|
|
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
|
-
: "
|
|
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 === "
|
|
1915
|
-
? "Codex hook approval is
|
|
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
|
|
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" | "
|
|
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
|
+
}
|