agenthooksprotocol 0.0.0-bootstrap.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +29 -0
- package/dist/src/client/auth.d.ts +147 -0
- package/dist/src/client/auth.d.ts.map +1 -0
- package/dist/src/client/auth.js +629 -0
- package/dist/src/client/auth.js.map +1 -0
- package/dist/src/client/composition.d.ts +42 -0
- package/dist/src/client/composition.d.ts.map +1 -0
- package/dist/src/client/composition.js +523 -0
- package/dist/src/client/composition.js.map +1 -0
- package/dist/src/client/content.d.ts +73 -0
- package/dist/src/client/content.d.ts.map +1 -0
- package/dist/src/client/content.js +404 -0
- package/dist/src/client/content.js.map +1 -0
- package/dist/src/client/hooks.d.ts +62 -0
- package/dist/src/client/hooks.d.ts.map +1 -0
- package/dist/src/client/hooks.js +997 -0
- package/dist/src/client/hooks.js.map +1 -0
- package/dist/src/client/index.d.ts +11 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +9 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/client/transport.d.ts +12 -0
- package/dist/src/client/transport.d.ts.map +1 -0
- package/dist/src/client/transport.js +347 -0
- package/dist/src/client/transport.js.map +1 -0
- package/dist/src/client/types.d.ts +117 -0
- package/dist/src/client/types.d.ts.map +1 -0
- package/dist/src/client/types.js +10 -0
- package/dist/src/client/types.js.map +1 -0
- package/dist/src/client/validation.d.ts +3 -0
- package/dist/src/client/validation.d.ts.map +1 -0
- package/dist/src/client/validation.js +25 -0
- package/dist/src/client/validation.js.map +1 -0
- package/dist/src/compaction.d.ts +27 -0
- package/dist/src/compaction.d.ts.map +1 -0
- package/dist/src/compaction.js +155 -0
- package/dist/src/compaction.js.map +1 -0
- package/dist/src/content-upload.d.ts +60 -0
- package/dist/src/content-upload.d.ts.map +1 -0
- package/dist/src/content-upload.js +129 -0
- package/dist/src/content-upload.js.map +1 -0
- package/dist/src/draft/generated.d.ts +3085 -0
- package/dist/src/draft/generated.d.ts.map +1 -0
- package/dist/src/draft/generated.js +15372 -0
- package/dist/src/draft/generated.js.map +1 -0
- package/dist/src/draft/index.d.ts +31 -0
- package/dist/src/draft/index.d.ts.map +1 -0
- package/dist/src/draft/index.js +129 -0
- package/dist/src/draft/index.js.map +1 -0
- package/dist/src/draft/schemas.d.ts +8848 -0
- package/dist/src/draft/schemas.d.ts.map +1 -0
- package/dist/src/draft/schemas.js +9219 -0
- package/dist/src/draft/schemas.js.map +1 -0
- package/dist/src/draft-atomic.d.ts +18 -0
- package/dist/src/draft-atomic.d.ts.map +1 -0
- package/dist/src/draft-atomic.js +102 -0
- package/dist/src/draft-atomic.js.map +1 -0
- package/dist/src/draft-runtime.d.ts +89 -0
- package/dist/src/draft-runtime.d.ts.map +1 -0
- package/dist/src/draft-runtime.js +242 -0
- package/dist/src/draft-runtime.js.map +1 -0
- package/dist/src/elicitation.d.ts +18 -0
- package/dist/src/elicitation.d.ts.map +1 -0
- package/dist/src/elicitation.js +191 -0
- package/dist/src/elicitation.js.map +1 -0
- package/dist/src/errors.d.ts +7 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +15 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/framing.d.ts +7 -0
- package/dist/src/framing.d.ts.map +1 -0
- package/dist/src/framing.js +39 -0
- package/dist/src/framing.js.map +1 -0
- package/dist/src/generated.d.ts +304 -0
- package/dist/src/generated.d.ts.map +1 -0
- package/dist/src/generated.js +1885 -0
- package/dist/src/generated.js.map +1 -0
- package/dist/src/hook-runner.d.ts +11 -0
- package/dist/src/hook-runner.d.ts.map +1 -0
- package/dist/src/hook-runner.js +223 -0
- package/dist/src/hook-runner.js.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/observation.d.ts +26 -0
- package/dist/src/observation.d.ts.map +1 -0
- package/dist/src/observation.js +28 -0
- package/dist/src/observation.js.map +1 -0
- package/dist/src/server/attachments.d.ts +29 -0
- package/dist/src/server/attachments.d.ts.map +1 -0
- package/dist/src/server/attachments.js +112 -0
- package/dist/src/server/attachments.js.map +1 -0
- package/dist/src/server/hooks.d.ts +20 -0
- package/dist/src/server/hooks.d.ts.map +1 -0
- package/dist/src/server/hooks.js +129 -0
- package/dist/src/server/hooks.js.map +1 -0
- package/dist/src/server/index.d.ts +7 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/index.js +4 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/stdio.d.ts +15 -0
- package/dist/src/server/stdio.d.ts.map +1 -0
- package/dist/src/server/stdio.js +69 -0
- package/dist/src/server/stdio.js.map +1 -0
- package/dist/src/transport.d.ts +7 -0
- package/dist/src/transport.d.ts.map +1 -0
- package/dist/src/transport.js +195 -0
- package/dist/src/transport.js.map +1 -0
- package/dist/src/types.d.ts +116 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +14 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/validation.d.ts +7 -0
- package/dist/src/validation.d.ts.map +1 -0
- package/dist/src/validation.js +209 -0
- package/dist/src/validation.js.map +1 -0
- package/package.json +64 -0
- package/src/client/auth.ts +1076 -0
- package/src/client/composition.ts +708 -0
- package/src/client/content.ts +510 -0
- package/src/client/hooks.ts +1368 -0
- package/src/client/index.ts +28 -0
- package/src/client/transport.ts +408 -0
- package/src/client/types.ts +173 -0
- package/src/client/validation.ts +24 -0
- package/src/compaction.ts +196 -0
- package/src/content-upload.ts +205 -0
- package/src/draft/README.md +16 -0
- package/src/draft/ahp-codegen.lock.json +123 -0
- package/src/draft/generated.ts +19143 -0
- package/src/draft/index.ts +246 -0
- package/src/draft/schemas.ts +9413 -0
- package/src/draft-atomic.ts +119 -0
- package/src/draft-runtime.ts +365 -0
- package/src/elicitation.ts +254 -0
- package/src/errors.ts +30 -0
- package/src/framing.ts +56 -0
- package/src/generated.ts +2408 -0
- package/src/hook-runner.ts +310 -0
- package/src/index.ts +8 -0
- package/src/observation.ts +65 -0
- package/src/server/attachments.ts +128 -0
- package/src/server/hooks.ts +205 -0
- package/src/server/index.ts +18 -0
- package/src/server/node-crypto.d.ts +7 -0
- package/src/server/stdio.ts +81 -0
- package/src/transport.ts +273 -0
- package/src/types.ts +124 -0
- package/src/validation.ts +372 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { validateEffect } from "./draft/index.js";
|
|
2
|
+
import type { Effect, Capabilities } from "./draft/index.js";
|
|
3
|
+
/** Synthetic pending-boundary evaluator. No external work occurs while staging. */
|
|
4
|
+
export interface PendingState {
|
|
5
|
+
input: Record<string, unknown>;
|
|
6
|
+
candidate: {
|
|
7
|
+
value: unknown;
|
|
8
|
+
supplier: string;
|
|
9
|
+
input: Record<string, unknown>;
|
|
10
|
+
} | null;
|
|
11
|
+
permission: "native" | "allow" | "ask";
|
|
12
|
+
approval: "pending" | "approved" | "not-required";
|
|
13
|
+
denied: boolean;
|
|
14
|
+
messages: string[];
|
|
15
|
+
}
|
|
16
|
+
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
|
|
17
|
+
function equal(a: unknown, b: unknown): boolean {
|
|
18
|
+
if (a === b) return true;
|
|
19
|
+
if (
|
|
20
|
+
a === null ||
|
|
21
|
+
b === null ||
|
|
22
|
+
typeof a !== "object" ||
|
|
23
|
+
typeof b !== "object"
|
|
24
|
+
)
|
|
25
|
+
return false;
|
|
26
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
27
|
+
const x = a as Record<string, unknown>,
|
|
28
|
+
y = b as Record<string, unknown>;
|
|
29
|
+
return (
|
|
30
|
+
Object.keys(x).length === Object.keys(y).length &&
|
|
31
|
+
Object.keys(x).every((k) => Object.hasOwn(y, k) && equal(x[k], y[k]))
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
export function object(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
export function stageResponse(
|
|
38
|
+
before: PendingState,
|
|
39
|
+
effects: readonly Effect[],
|
|
40
|
+
capabilities: Capabilities,
|
|
41
|
+
supplier: string,
|
|
42
|
+
validateInput: (input: Record<string, unknown>) => boolean = (input) =>
|
|
43
|
+
Number.isInteger(input.task) && (input.task as number) > 0,
|
|
44
|
+
): PendingState {
|
|
45
|
+
const staged = clone(before);
|
|
46
|
+
for (const effect of effects) {
|
|
47
|
+
if (!validateEffect(effect).ok) throw new Error("Invalid effect");
|
|
48
|
+
if (!capabilities.effects.includes(effect.type))
|
|
49
|
+
throw new Error("Unsupported effect");
|
|
50
|
+
if (effect.type === "modify") {
|
|
51
|
+
if (
|
|
52
|
+
effect.target !== "input" ||
|
|
53
|
+
(effect.operation !== "replace" && effect.operation !== "merge") ||
|
|
54
|
+
capabilities.modify?.input?.[effect.operation] !== true
|
|
55
|
+
)
|
|
56
|
+
throw new Error("Unsupported target or operation");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Input mutations precede binding a result regardless of encoding order.
|
|
60
|
+
for (const effect of effects.filter((e) => e.type === "modify")) {
|
|
61
|
+
if (!object(effect.value)) throw new Error("Input must be an object");
|
|
62
|
+
if (effect.operation === "replace") staged.input = clone(effect.value);
|
|
63
|
+
else if (effect.operation === "merge")
|
|
64
|
+
staged.input = { ...staged.input, ...clone(effect.value) };
|
|
65
|
+
else throw new Error("Unsupported operation");
|
|
66
|
+
}
|
|
67
|
+
// Validate the final effective operation, never a partially staged replacement.
|
|
68
|
+
if (!validateInput(staged.input))
|
|
69
|
+
throw new Error("Invalid effective tool input");
|
|
70
|
+
if (!equal(before.input, staged.input)) {
|
|
71
|
+
staged.candidate = null;
|
|
72
|
+
if (staged.permission === "allow") staged.permission = "native";
|
|
73
|
+
if (staged.approval === "approved") staged.approval = "pending";
|
|
74
|
+
}
|
|
75
|
+
for (const effect of effects) {
|
|
76
|
+
switch (effect.type) {
|
|
77
|
+
case "modify":
|
|
78
|
+
break;
|
|
79
|
+
case "return":
|
|
80
|
+
staged.candidate = {
|
|
81
|
+
value: clone(effect.value),
|
|
82
|
+
supplier,
|
|
83
|
+
input: clone(staged.input),
|
|
84
|
+
};
|
|
85
|
+
break;
|
|
86
|
+
case "message":
|
|
87
|
+
if (typeof effect.text !== "string") throw new Error("Invalid message");
|
|
88
|
+
staged.messages.push(effect.text);
|
|
89
|
+
break;
|
|
90
|
+
case "deny":
|
|
91
|
+
staged.denied = true;
|
|
92
|
+
break;
|
|
93
|
+
case "ask":
|
|
94
|
+
staged.permission = "ask";
|
|
95
|
+
staged.approval = "pending";
|
|
96
|
+
break;
|
|
97
|
+
case "allow":
|
|
98
|
+
if (staged.permission !== "ask") staged.permission = "allow";
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
throw new Error("Unsupported effect");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (staged.denied) staged.candidate = null;
|
|
105
|
+
return staged;
|
|
106
|
+
}
|
|
107
|
+
export function decide(
|
|
108
|
+
state: PendingState,
|
|
109
|
+
managedPolicy: "allow" | "deny",
|
|
110
|
+
): string[] {
|
|
111
|
+
const actions = ["policy"];
|
|
112
|
+
if (state.denied || managedPolicy === "deny") actions.push("blocked");
|
|
113
|
+
else if (state.approval === "pending" && state.permission !== "allow")
|
|
114
|
+
actions.push("approval-required");
|
|
115
|
+
else if (state.candidate !== null)
|
|
116
|
+
actions.push(`candidate:${state.candidate.supplier}`);
|
|
117
|
+
else actions.push("execute");
|
|
118
|
+
return actions;
|
|
119
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { validateEffect } from "./draft/index.js";
|
|
2
|
+
import { validateBodyReference, type BodyReference } from "./content-upload.js";
|
|
3
|
+
/** Synthetic semantic harness. Wire acceptance remains the canonical validator's job. */
|
|
4
|
+
import { stageResponse, object, type PendingState } from "./draft-atomic.js";
|
|
5
|
+
import type { Effect, Capabilities } from "./draft/index.js";
|
|
6
|
+
export interface BoundaryState extends PendingState {
|
|
7
|
+
flow?: "stop" | "continue";
|
|
8
|
+
instructions?: string[];
|
|
9
|
+
continuationRemaining?: number;
|
|
10
|
+
injections?: unknown[];
|
|
11
|
+
values?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface BoundaryCapabilities {
|
|
14
|
+
effects: string[];
|
|
15
|
+
modify?: Record<
|
|
16
|
+
string,
|
|
17
|
+
{ replace?: boolean; merge?: boolean; operations?: string[] }
|
|
18
|
+
>;
|
|
19
|
+
flow?: {
|
|
20
|
+
operations: string[];
|
|
21
|
+
remainingContinuations?: number;
|
|
22
|
+
continuationCount?: number;
|
|
23
|
+
maxContinuations?: number;
|
|
24
|
+
};
|
|
25
|
+
inject?: { context: { append: boolean; deliverAt: string[] } };
|
|
26
|
+
}
|
|
27
|
+
export type BoundaryEffect = { type: string; [key: string]: unknown };
|
|
28
|
+
export function stageBoundary(
|
|
29
|
+
before: BoundaryState,
|
|
30
|
+
effects: readonly BoundaryEffect[],
|
|
31
|
+
caps: BoundaryCapabilities,
|
|
32
|
+
supplier: string,
|
|
33
|
+
validateInput: (input: Record<string, unknown>) => boolean = () => true,
|
|
34
|
+
): BoundaryState {
|
|
35
|
+
// Stage all state, including scheduling and user-facing messages, before the single commit.
|
|
36
|
+
let staged = structuredClone(before);
|
|
37
|
+
if (
|
|
38
|
+
staged.continuationRemaining === undefined &&
|
|
39
|
+
caps.flow?.remainingContinuations !== undefined
|
|
40
|
+
)
|
|
41
|
+
staged.continuationRemaining = caps.flow.remainingContinuations;
|
|
42
|
+
const ordinary: BoundaryEffect[] = [];
|
|
43
|
+
for (const effect of effects) {
|
|
44
|
+
if (!validateEffect(effect).ok) throw Error("Invalid effect");
|
|
45
|
+
if (!caps.effects.includes(effect.type)) throw Error("Unsupported effect");
|
|
46
|
+
if (effect.type === "flow") {
|
|
47
|
+
const operation = effect.operation;
|
|
48
|
+
if (
|
|
49
|
+
typeof operation !== "string" ||
|
|
50
|
+
!caps.flow?.operations.includes(operation)
|
|
51
|
+
)
|
|
52
|
+
throw Error("Unsupported flow operation");
|
|
53
|
+
if (operation === "continue") {
|
|
54
|
+
if (
|
|
55
|
+
(!(Number(staged.continuationRemaining) > 0) &&
|
|
56
|
+
before.flow !== "continue") ||
|
|
57
|
+
(caps.flow.maxContinuations !== undefined &&
|
|
58
|
+
(caps.flow.continuationCount ?? 0) >= caps.flow.maxContinuations)
|
|
59
|
+
)
|
|
60
|
+
throw Error("Continuation unavailable");
|
|
61
|
+
if (staged.flow !== "stop") staged.flow = "continue";
|
|
62
|
+
if (typeof effect.instruction === "string")
|
|
63
|
+
staged.instructions = [
|
|
64
|
+
...(staged.instructions ?? []),
|
|
65
|
+
effect.instruction,
|
|
66
|
+
];
|
|
67
|
+
} else if (operation === "stop" && typeof effect.reason === "string")
|
|
68
|
+
staged.flow = "stop";
|
|
69
|
+
else throw Error("Invalid flow");
|
|
70
|
+
} else if (effect.type === "inject") {
|
|
71
|
+
if (
|
|
72
|
+
typeof effect.deliverAt !== "string" ||
|
|
73
|
+
!caps.inject?.context.deliverAt.includes(effect.deliverAt) ||
|
|
74
|
+
caps.inject.context.append !== true ||
|
|
75
|
+
effect.target !== "context" ||
|
|
76
|
+
effect.operation !== "append" ||
|
|
77
|
+
!Object.hasOwn(effect, "value")
|
|
78
|
+
)
|
|
79
|
+
throw Error("Unsupported injection");
|
|
80
|
+
staged.injections = [
|
|
81
|
+
...(staged.injections ?? []),
|
|
82
|
+
structuredClone(effect),
|
|
83
|
+
];
|
|
84
|
+
} else if (effect.type === "modify") {
|
|
85
|
+
const target = effect.target,
|
|
86
|
+
operation = effect.operation;
|
|
87
|
+
if (
|
|
88
|
+
typeof target !== "string" ||
|
|
89
|
+
(operation !== "replace" && operation !== "merge")
|
|
90
|
+
)
|
|
91
|
+
throw Error("Unsupported modification");
|
|
92
|
+
const targetCaps = caps.modify?.[target];
|
|
93
|
+
if (
|
|
94
|
+
!(
|
|
95
|
+
targetCaps?.[operation] === true ||
|
|
96
|
+
targetCaps?.operations?.includes(operation)
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
throw Error("Unsupported target or operation");
|
|
100
|
+
if (target === "input") ordinary.push(effect);
|
|
101
|
+
else {
|
|
102
|
+
if (!staged.values || !Object.hasOwn(staged.values, target))
|
|
103
|
+
throw Error("Target unavailable");
|
|
104
|
+
const previous = staged.values[target];
|
|
105
|
+
if (
|
|
106
|
+
operation === "merge" &&
|
|
107
|
+
(!object(previous) || !object(effect.value))
|
|
108
|
+
)
|
|
109
|
+
throw Error("Merge requires objects");
|
|
110
|
+
staged.values[target] =
|
|
111
|
+
operation === "merge"
|
|
112
|
+
? {
|
|
113
|
+
...(previous as object),
|
|
114
|
+
...(structuredClone(effect.value) as object),
|
|
115
|
+
}
|
|
116
|
+
: structuredClone(effect.value);
|
|
117
|
+
}
|
|
118
|
+
} else ordinary.push(effect);
|
|
119
|
+
}
|
|
120
|
+
if (!sameValue(before.values, staged.values)) {
|
|
121
|
+
staged.candidate = null;
|
|
122
|
+
if (staged.permission === "allow") staged.permission = "native";
|
|
123
|
+
if (staged.approval === "approved") staged.approval = "pending";
|
|
124
|
+
}
|
|
125
|
+
const inputCaps = caps.modify?.input;
|
|
126
|
+
const normalized = {
|
|
127
|
+
effects: caps.effects,
|
|
128
|
+
modify: {
|
|
129
|
+
input: {
|
|
130
|
+
replace:
|
|
131
|
+
inputCaps?.replace === true ||
|
|
132
|
+
inputCaps?.operations?.includes("replace") === true,
|
|
133
|
+
merge:
|
|
134
|
+
inputCaps?.merge === true ||
|
|
135
|
+
inputCaps?.operations?.includes("merge") === true,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
staged = stageResponse(
|
|
140
|
+
staged,
|
|
141
|
+
ordinary as Effect[],
|
|
142
|
+
normalized as Capabilities,
|
|
143
|
+
supplier,
|
|
144
|
+
validateInput,
|
|
145
|
+
);
|
|
146
|
+
if (staged.continuationRemaining !== undefined) {
|
|
147
|
+
if (staged.flow === "continue" && before.flow !== "continue")
|
|
148
|
+
staged.continuationRemaining--;
|
|
149
|
+
else if (staged.flow === "stop" && before.flow === "continue")
|
|
150
|
+
staged.continuationRemaining++;
|
|
151
|
+
}
|
|
152
|
+
return staged;
|
|
153
|
+
}
|
|
154
|
+
export interface Delivery {
|
|
155
|
+
mode: "intercept" | "observe";
|
|
156
|
+
capabilities: BoundaryCapabilities;
|
|
157
|
+
receive(view: BoundaryState & { eventId: string }): Promise<BoundaryEffect[]>;
|
|
158
|
+
}
|
|
159
|
+
export async function dispatchBoundary(
|
|
160
|
+
eventId: string,
|
|
161
|
+
initial: BoundaryState,
|
|
162
|
+
deliveries: Delivery[],
|
|
163
|
+
): Promise<BoundaryState> {
|
|
164
|
+
let state = structuredClone(initial);
|
|
165
|
+
for (const [index, delivery] of deliveries.entries())
|
|
166
|
+
if (delivery.mode === "intercept") {
|
|
167
|
+
const effects = await delivery.receive({
|
|
168
|
+
...structuredClone(state),
|
|
169
|
+
eventId,
|
|
170
|
+
});
|
|
171
|
+
state = stageBoundary(
|
|
172
|
+
state,
|
|
173
|
+
effects,
|
|
174
|
+
delivery.capabilities,
|
|
175
|
+
String(index),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
// Observation is a settled snapshot, not a second effect channel or execution gate.
|
|
179
|
+
void Promise.all(
|
|
180
|
+
deliveries
|
|
181
|
+
.filter((d) => d.mode === "observe")
|
|
182
|
+
.map(async (delivery) => {
|
|
183
|
+
try {
|
|
184
|
+
await delivery.receive({ ...structuredClone(state), eventId });
|
|
185
|
+
} catch {
|
|
186
|
+
/* best effort */
|
|
187
|
+
}
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
return state;
|
|
191
|
+
}
|
|
192
|
+
export interface ContentItem {
|
|
193
|
+
id: string;
|
|
194
|
+
kind: string;
|
|
195
|
+
ref?: string;
|
|
196
|
+
bytes?: Uint8Array;
|
|
197
|
+
}
|
|
198
|
+
export interface ContentView {
|
|
199
|
+
id: string;
|
|
200
|
+
kind: string;
|
|
201
|
+
ref?: string;
|
|
202
|
+
sizeBytes?: number;
|
|
203
|
+
sha256?: string;
|
|
204
|
+
gap?:
|
|
205
|
+
| "not_requested"
|
|
206
|
+
| "permission_withheld"
|
|
207
|
+
| "source_unavailable"
|
|
208
|
+
| "transfer_failed"
|
|
209
|
+
| "limit_exceeded";
|
|
210
|
+
}
|
|
211
|
+
/** Receiver storage scoped by the host's authenticated principal, not wire claims. */
|
|
212
|
+
export class ContentReceiver {
|
|
213
|
+
private stored = new Map<string, Uint8Array>();
|
|
214
|
+
async upload(scope: string, bytes: Uint8Array): Promise<BodyReference> {
|
|
215
|
+
if (!scope || !(bytes instanceof Uint8Array))
|
|
216
|
+
throw Error("Authenticated scope and binary bytes required");
|
|
217
|
+
const snapshot = bytes.slice();
|
|
218
|
+
const digest = new Uint8Array(
|
|
219
|
+
await crypto.subtle.digest("SHA-256", snapshot),
|
|
220
|
+
);
|
|
221
|
+
const sha256 = Array.from(digest, (byte) =>
|
|
222
|
+
byte.toString(16).padStart(2, "0"),
|
|
223
|
+
).join("");
|
|
224
|
+
const ref = "urn:ahp:content:" + crypto.randomUUID();
|
|
225
|
+
this.stored.set(JSON.stringify([scope, ref]), snapshot);
|
|
226
|
+
return { ref, size: snapshot.length, sha256 };
|
|
227
|
+
}
|
|
228
|
+
read(scope: string, ref: string): Uint8Array | undefined {
|
|
229
|
+
return this.stored.get(JSON.stringify([scope, ref]))?.slice();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export async function prepareContent(
|
|
233
|
+
items: ContentItem[],
|
|
234
|
+
options: {
|
|
235
|
+
subscription: string;
|
|
236
|
+
requested: string[];
|
|
237
|
+
authorized: string[];
|
|
238
|
+
maxBytes: number;
|
|
239
|
+
mode: "intercept" | "observe";
|
|
240
|
+
failClosed: boolean;
|
|
241
|
+
timeoutMs?: number;
|
|
242
|
+
upload(bytes: Uint8Array): Promise<BodyReference>;
|
|
243
|
+
},
|
|
244
|
+
): Promise<ContentView[]> {
|
|
245
|
+
const result: ContentView[] = [];
|
|
246
|
+
for (const item of items) {
|
|
247
|
+
const view: ContentView = { id: item.id, kind: item.kind };
|
|
248
|
+
if (!options.requested.includes(item.kind)) view.gap = "not_requested";
|
|
249
|
+
else if (!options.authorized.includes(item.kind))
|
|
250
|
+
view.gap = "permission_withheld";
|
|
251
|
+
else if (item.bytes === undefined) view.gap = "source_unavailable";
|
|
252
|
+
else if (item.bytes.length > options.maxBytes) view.gap = "limit_exceeded";
|
|
253
|
+
else
|
|
254
|
+
try {
|
|
255
|
+
const bytes = new Uint8Array(item.bytes);
|
|
256
|
+
const hash = new Uint8Array(
|
|
257
|
+
await crypto.subtle.digest("SHA-256", bytes),
|
|
258
|
+
);
|
|
259
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
260
|
+
let confirmed: BodyReference;
|
|
261
|
+
try {
|
|
262
|
+
confirmed = await Promise.race([
|
|
263
|
+
options.upload(bytes.slice()),
|
|
264
|
+
new Promise<never>((_, reject) => {
|
|
265
|
+
timer = setTimeout(
|
|
266
|
+
() => reject(Error("Upload timeout")),
|
|
267
|
+
options.timeoutMs ?? 1000,
|
|
268
|
+
);
|
|
269
|
+
}),
|
|
270
|
+
]);
|
|
271
|
+
} finally {
|
|
272
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
273
|
+
}
|
|
274
|
+
const sha256 = Array.from(hash, (byte) =>
|
|
275
|
+
byte.toString(16).padStart(2, "0"),
|
|
276
|
+
).join("");
|
|
277
|
+
const body = validateBodyReference(confirmed, bytes.length, sha256);
|
|
278
|
+
view.ref = body.ref;
|
|
279
|
+
view.sizeBytes = body.size;
|
|
280
|
+
view.sha256 = body.sha256;
|
|
281
|
+
} catch {
|
|
282
|
+
view.gap = "transfer_failed";
|
|
283
|
+
}
|
|
284
|
+
if (
|
|
285
|
+
view.gap &&
|
|
286
|
+
view.gap !== "not_requested" &&
|
|
287
|
+
options.mode === "intercept" &&
|
|
288
|
+
options.failClosed
|
|
289
|
+
)
|
|
290
|
+
throw Error("Content delivery failure");
|
|
291
|
+
result.push(view);
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
export class Lineage {
|
|
296
|
+
private parents = new Map<string, string | undefined>();
|
|
297
|
+
add(id: string, parent?: string): void {
|
|
298
|
+
if (this.parents.has(id)) throw Error("Duplicate event");
|
|
299
|
+
const seen = new Set([id]);
|
|
300
|
+
for (
|
|
301
|
+
let cursor = parent;
|
|
302
|
+
cursor !== undefined;
|
|
303
|
+
cursor = this.parents.get(cursor)
|
|
304
|
+
) {
|
|
305
|
+
if (seen.has(cursor)) throw Error("Cyclic lineage");
|
|
306
|
+
seen.add(cursor);
|
|
307
|
+
}
|
|
308
|
+
this.parents.set(id, parent);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function sameValue(a: unknown, b: unknown): boolean {
|
|
312
|
+
if (a === b) return true;
|
|
313
|
+
if (
|
|
314
|
+
a === null ||
|
|
315
|
+
b === null ||
|
|
316
|
+
typeof a !== "object" ||
|
|
317
|
+
typeof b !== "object" ||
|
|
318
|
+
Array.isArray(a) !== Array.isArray(b)
|
|
319
|
+
)
|
|
320
|
+
return false;
|
|
321
|
+
const left = a as Record<string, unknown>,
|
|
322
|
+
right = b as Record<string, unknown>;
|
|
323
|
+
return (
|
|
324
|
+
Object.keys(left).length === Object.keys(right).length &&
|
|
325
|
+
Object.keys(left).every(
|
|
326
|
+
(key) => Object.hasOwn(right, key) && sameValue(left[key], right[key]),
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
export function actualTaskChange<T>(
|
|
331
|
+
before: T,
|
|
332
|
+
after: T,
|
|
333
|
+
id: string,
|
|
334
|
+
parentEventId?: string,
|
|
335
|
+
): {
|
|
336
|
+
id: string;
|
|
337
|
+
type: string;
|
|
338
|
+
parentEventId?: string;
|
|
339
|
+
before: T;
|
|
340
|
+
after: T;
|
|
341
|
+
} | null {
|
|
342
|
+
// Arguments are committed snapshots, not proposed values. Denial with no write
|
|
343
|
+
// has equal snapshots; actual partial changes remain observable after refusal.
|
|
344
|
+
if (sameValue(before, after)) return null;
|
|
345
|
+
return {
|
|
346
|
+
id,
|
|
347
|
+
type: "task.change.after",
|
|
348
|
+
...(parentEventId === undefined ? {} : { parentEventId }),
|
|
349
|
+
before: structuredClone(before),
|
|
350
|
+
after: structuredClone(after),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
export { uploadContent, prepareWireContent } from "./content-upload.js";
|
|
354
|
+
export type {
|
|
355
|
+
UploadConfiguration,
|
|
356
|
+
BodyReference,
|
|
357
|
+
NormalizedContentInput,
|
|
358
|
+
NormalizedContentView,
|
|
359
|
+
} from "./content-upload.js";
|
|
360
|
+
export { dispatchObservations } from "./observation.js";
|
|
361
|
+
export type {
|
|
362
|
+
ObservationEvent,
|
|
363
|
+
ObservationSubscription,
|
|
364
|
+
ObservationNotification,
|
|
365
|
+
} from "./observation.js";
|