fix(audit): ignore inherited execution identity evidence (#122418)

* fix(audit): require owned admission data

* docs(agents): preserve owned admission invariant
This commit is contained in:
Josh Avant
2026-08-11 23:19:17 -05:00
committed by GitHub
parent f6fff4f7fd
commit 35d764076e
4 changed files with 753 additions and 141 deletions
+1 -1
View File
@@ -172,7 +172,7 @@ Skills own workflows; root owns hard policy and routing. Product direction and m
- Execution identity is opt-in diagnostic provenance, never authorization or enforcement. Unknown facts stay unknown; record ingress or invoker facts only at their authoritative producer. Never infer identity from session keys, `runId`, or routing metadata.
- Invoker evidence is tri-state: tagged principal-bearing input is `present`, tagged principal-less input is `unknown`, and omission alone is `absent`. Validate the closed raw variant before projection or field dropping; reject malformed, mixed, untagged, or extra-field input instead of normalizing it to `unknown` or absence.
- Each outer admitted turn owns one immutable `executionId` and `contextId`; `runId` is non-unique correlation. Retries, fallbacks, and recovery reuse the original admission identity. Only byte-identical canonical replay is idempotent.
- Admission may only validate, bound, freeze, and enqueue through the shared audit writer. No synchronous SQLite, schema, filesystem, HMAC-key, or readiness work. Audit failure never delays or aborts execution.
- Admission may only validate, bound, freeze, and enqueue through the shared audit writer. Admission validates only a recursively owned, enumerable, accessor-free data snapshot constructed from descriptors before schema checks or ordinary property reads; inherited properties are absent and accessors never run. No synchronous SQLite, schema, filesystem, HMAC-key, or readiness work. Audit failure never delays or aborts execution.
- Raw identity references are transient worker-message data. Never persist, export, inspect, or log them. Public Plugin SDK ingress must strip private recovery/admission authority, including JavaScript extra and inherited properties.
- Default or disabled collection creates and propagates no identity token and does not create optional storage. Existing-storage maintenance may continue. Reads enforce expiry before projection; missing or expired evidence never proves no run occurred.
- `audit.run.inspect` intentionally uses `operator.read` within one trusted Gateway domain. Reader isolation requires separate domains. Ask before changing this scope, default-off behavior, retained fields, 30-day cutoff, maintenance/row bounds, or schema/protocol contract.
+160 -32
View File
@@ -19,6 +19,11 @@ import {
processExecutionIdentityAdmissionWork,
} from "./execution-identity-context.js";
function defineObjectPrototypeProperties(descriptors: PropertyDescriptorMap): void {
// oxlint-disable-next-line no-extend-native -- Exercise hostile prototype pollution across the real worker boundary.
Object.defineProperties(Object.prototype, descriptors);
}
function captureExecutionIdentityAdmissionEnvelope(
facts: ExecutionIdentityAdmissionFacts,
options: {
@@ -229,45 +234,151 @@ describe("audit event worker", () => {
}
});
it("preserves explicit unknown invoker evidence through the worker clone boundary", async () => {
it("persists owned unknown and omits inherited evidence through the worker clone boundary", async () => {
const stateDir = tempDirs.make("openclaw-audit-writer-");
const database = { env: { OPENCLAW_STATE_DIR: stateDir } };
const errors: string[] = [];
const writer = createAuditEventWriter({ stateDir, onError: (error) => errors.push(error) });
const clearSink = configureExecutionIdentityAdmissionSink(writer.recordExecutionIdentity);
const admittedAt = Date.now();
expect(
enqueueExecutionIdentityContextAtAdmission(
{
runId: "unknown-invoker-run",
agentId: "main",
ingress: { kind: "local-cli", boundary: "agent-command.local", state: "present" },
runtime: { kind: "embedded" },
invoker: { state: "unknown" },
},
{
enabled: true,
contextId: "unknown-invoker-context",
executionId: "unknown-invoker-execution",
now: admittedAt,
runtimeInstanceId: "private-runtime-reference",
},
),
).toEqual({
candidateContextId: "unknown-invoker-context",
candidateExecutionId: "unknown-invoker-execution",
accepted: true,
});
clearSink();
await writer.stop();
const inspected = inspectExecutionIdentityRun(
{ executionId: "unknown-invoker-execution" },
{ ...database, now: admittedAt },
const inheritedRefs = {
invoker: "raw-inherited-principal",
applicableGrants: "raw-inherited-grant",
assurance: "raw-inherited-assurance",
rawSourceRef: "raw-inherited-source",
} as const;
const prior = new Map(
Object.keys(inheritedRefs).map((key) => [
key,
Object.getOwnPropertyDescriptor(Object.prototype, key),
]),
);
let inheritedInvokerReads = 0;
try {
try {
defineObjectPrototypeProperties({
invoker: {
configurable: true,
enumerable: false,
get: () => {
inheritedInvokerReads += 1;
return {
state: "present",
kind: "local-account",
rawPrincipalRef: inheritedRefs.invoker,
};
},
},
applicableGrants: {
configurable: true,
enumerable: false,
value: [{ rawGrantRef: inheritedRefs.applicableGrants, state: "present" }],
},
assurance: {
configurable: true,
enumerable: false,
value: [
{
kind: "other",
rawEvidenceRef: inheritedRefs.assurance,
strength: "self-asserted",
},
],
},
rawSourceRef: {
configurable: true,
enumerable: false,
value: inheritedRefs.rawSourceRef,
},
});
expect(
enqueueExecutionIdentityContextAtAdmission(
{
runId: "absent-invoker-run",
agentId: "main",
ingress: {
kind: "local-cli",
boundary: "agent-command.local",
state: "present",
},
runtime: { kind: "embedded" },
},
{
enabled: true,
contextId: "absent-invoker-context",
executionId: "absent-invoker-execution",
now: admittedAt,
runtimeInstanceId: "private-absent-runtime-reference",
},
),
).toEqual({
candidateContextId: "absent-invoker-context",
candidateExecutionId: "absent-invoker-execution",
accepted: true,
});
} finally {
for (const [key, descriptor] of prior) {
if (descriptor) {
defineObjectPrototypeProperties({ [key]: descriptor });
} else {
delete (Object.prototype as Record<string, unknown>)[key];
}
}
}
expect(
enqueueExecutionIdentityContextAtAdmission(
{
runId: "unknown-invoker-run",
agentId: "main",
ingress: { kind: "local-cli", boundary: "agent-command.local", state: "present" },
runtime: { kind: "embedded" },
invoker: { state: "unknown" },
},
{
enabled: true,
contextId: "unknown-invoker-context",
executionId: "unknown-invoker-execution",
now: admittedAt + 1,
runtimeInstanceId: "private-unknown-runtime-reference",
},
),
).toEqual({
candidateContextId: "unknown-invoker-context",
candidateExecutionId: "unknown-invoker-execution",
accepted: true,
});
} finally {
clearSink();
await writer.stop();
}
const absentInspection = inspectExecutionIdentityRun(
{ executionId: "absent-invoker-execution" },
{ ...database, now: admittedAt + 1 },
);
const unknownInspection = inspectExecutionIdentityRun(
{ executionId: "unknown-invoker-execution" },
{ ...database, now: admittedAt + 1 },
);
expect(inheritedInvokerReads).toBe(0);
expect(errors).toEqual([]);
expect(inspected).toMatchObject({
expect(absentInspection).toMatchObject({
identity: {
state: "present",
context: {
invoker: { state: "absent" },
ingress: { state: "present" },
applicableGrants: [],
assurance: [{ kind: "runtime-binding", strength: "boundary-verified" }],
coverageState: "unattributed",
missingEvidence: ["invoker.principal"],
},
},
coverage: { state: "unattributed", missingEvidence: ["invoker.principal"] },
});
expect(unknownInspection).toMatchObject({
identity: {
state: "present",
context: {
@@ -278,7 +389,24 @@ describe("audit event worker", () => {
},
coverage: { state: "unknown", missingEvidence: ["invoker.principal"] },
});
expect(JSON.stringify(inspected)).not.toContain("private-runtime-reference");
const persisted = openOpenClawStateDatabase(database)
.db.prepare(
"SELECT context_json FROM execution_identity_contexts WHERE execution_id IN (?, ?) ORDER BY execution_id",
)
.all("absent-invoker-execution", "unknown-invoker-execution") as Array<{
context_json: string;
}>;
const publicAndStored = JSON.stringify({
errors,
absentInspection,
unknownInspection,
persisted,
});
for (const rawRef of Object.values(inheritedRefs)) {
expect(publicAndStored).not.toContain(rawRef);
}
expect(publicAndStored).not.toContain("private-absent-runtime-reference");
expect(publicAndStored).not.toContain("private-unknown-runtime-reference");
});
it("prunes expired identity contexts before preserving exact-envelope conflicts", async () => {
@@ -5,6 +5,7 @@ import {
enqueueExecutionIdentityContextAtAdmission,
hasExecutionIdentityAdmissionSink,
parseExecutionIdentityAdmissionEnvelope,
parseExecutionIdentityAdmissionWork,
type ExecutionIdentityAdmissionEnvelope,
type ExecutionIdentityAdmissionFacts,
type ExecutionIdentityAdmissionWork,
@@ -13,6 +14,22 @@ import {
const ADMISSION_MAX_BYTES = 16 * 1024;
const ADMISSION_MAX_ITEMS = 16;
function defineObjectPrototypeProperty(key: string, descriptor: PropertyDescriptor): void {
// oxlint-disable-next-line no-extend-native -- Exercise hostile prototype pollution at the admission boundary.
Object.defineProperty(Object.prototype, key, descriptor);
}
function restoreObjectPrototypeProperty(
key: string,
descriptor: PropertyDescriptor | undefined,
): void {
if (descriptor) {
defineObjectPrototypeProperty(key, descriptor);
} else {
delete (Object.prototype as Record<string, unknown>)[key];
}
}
function facts(overrides: Partial<ExecutionIdentityAdmissionFacts> = {}) {
return {
runId: "run-1",
@@ -150,6 +167,379 @@ describe("execution identity admission envelope", () => {
}
});
it("omits inherited outer evidence instead of projecting it", () => {
const inheritedRefs = {
invoker: { state: "unknown" },
applicableGrants: [{ rawGrantRef: "inherited-grant", state: "present" }],
assurance: [
{
kind: "other",
rawEvidenceRef: "inherited-assurance",
strength: "self-asserted",
},
],
} as const;
const prior = new Map(
Object.keys(inheritedRefs).map((key) => [
key,
Object.getOwnPropertyDescriptor(Object.prototype, key),
]),
);
let envelope: ExecutionIdentityAdmissionEnvelope;
try {
for (const [key, value] of Object.entries(inheritedRefs)) {
defineObjectPrototypeProperty(key, {
configurable: true,
enumerable: false,
value,
writable: true,
});
}
envelope = captureEnvelope(facts(), {
contextId: "context-inherited",
executionId: "execution-inherited",
now: 1,
runtimeInstanceId: "runtime-owned",
});
} finally {
for (const [key, descriptor] of prior) {
restoreObjectPrototypeProperty(key, descriptor);
}
}
expect(Object.hasOwn(envelope!, "invoker")).toBe(false);
expect(envelope!.applicableGrants).toEqual([]);
expect(envelope!.assurance).toEqual([
{
kind: "runtime-binding",
rawEvidenceRef: "runtime-owned",
strength: "boundary-verified",
},
]);
});
it("never reads inherited accessors while treating optional evidence as omitted", () => {
const keys = ["invoker", "applicableGrants", "assurance"] as const;
const prior = new Map(
keys.map((key) => [key, Object.getOwnPropertyDescriptor(Object.prototype, key)]),
);
const getterReads = new Map(keys.map((key) => [key, 0]));
let envelope: ExecutionIdentityAdmissionEnvelope;
try {
for (const key of keys) {
defineObjectPrototypeProperty(key, {
configurable: true,
enumerable: false,
get: () => {
getterReads.set(key, getterReads.get(key)! + 1);
return key === "invoker"
? { state: "unknown" }
: key === "applicableGrants"
? [{ rawGrantRef: "inherited-grant", state: "present" }]
: [
{
kind: "other",
rawEvidenceRef: "inherited-assurance",
strength: "self-asserted",
},
];
},
});
}
envelope = captureEnvelope(facts(), {
contextId: "context-inherited-getter",
executionId: "execution-inherited-getter",
now: 1,
runtimeInstanceId: "runtime-owned",
});
} finally {
for (const [key, descriptor] of prior) {
restoreObjectPrototypeProperty(key, descriptor);
}
}
expect(Object.fromEntries(getterReads)).toEqual({
invoker: 0,
applicableGrants: 0,
assurance: 0,
});
expect(Object.hasOwn(envelope!, "invoker")).toBe(false);
expect(envelope!.applicableGrants).toEqual([]);
expect(envelope!.assurance).toEqual([
{
kind: "runtime-binding",
rawEvidenceRef: "runtime-owned",
strength: "boundary-verified",
},
]);
});
it.each([
{
name: "ingress state",
key: "state",
value: "unknown",
admissionFacts: () => facts(),
assertOmitted: (envelope: ExecutionIdentityAdmissionEnvelope) => {
expect(envelope.ingress.state).toBe("present");
},
},
{
name: "ingress source",
key: "rawSourceRef",
value: "inherited-source",
admissionFacts: () => facts(),
assertOmitted: (envelope: ExecutionIdentityAdmissionEnvelope) => {
expect(Object.hasOwn(envelope.ingress, "rawSourceRef")).toBe(false);
},
},
{
name: "invoker label",
key: "displayLabel",
value: "inherited-label",
admissionFacts: () =>
facts({
invoker: {
state: "present",
kind: "local-account",
rawPrincipalRef: "owned-principal",
},
}),
assertOmitted: (envelope: ExecutionIdentityAdmissionEnvelope) => {
expect(envelope.invoker?.state).toBe("present");
expect(Object.hasOwn(envelope.invoker!, "displayLabel")).toBe(false);
},
},
])("omits inherited optional $name data", ({ key, value, admissionFacts, assertOmitted }) => {
const prior = Object.getOwnPropertyDescriptor(Object.prototype, key);
let dataEnvelope: ExecutionIdentityAdmissionEnvelope;
let getterEnvelope: ExecutionIdentityAdmissionEnvelope;
let getterReads = 0;
try {
defineObjectPrototypeProperty(key, {
configurable: true,
enumerable: false,
value,
writable: true,
});
dataEnvelope = captureEnvelope(admissionFacts(), {
contextId: `context-${key}`,
executionId: `execution-${key}`,
now: 1,
runtimeInstanceId: "runtime-owned",
});
defineObjectPrototypeProperty(key, {
configurable: true,
enumerable: false,
get: () => {
getterReads += 1;
return value;
},
});
getterEnvelope = captureEnvelope(admissionFacts(), {
contextId: `context-${key}-getter`,
executionId: `execution-${key}-getter`,
now: 2,
runtimeInstanceId: "runtime-owned",
});
} finally {
restoreObjectPrototypeProperty(key, prior);
}
expect(getterReads).toBe(0);
assertOmitted(dataEnvelope!);
assertOmitted(getterEnvelope!);
});
it.each([
["outer run id", "runId", "inherited-run", () => omitOwn(facts(), "runId")],
["outer agent id", "agentId", "inherited-agent", () => omitOwn(facts(), "agentId")],
["outer ingress", "ingress", facts().ingress, () => omitOwn(facts(), "ingress")],
["outer runtime", "runtime", facts().runtime, () => omitOwn(facts(), "runtime")],
[
"ingress kind",
"kind",
"local-cli",
() => facts({ ingress: { boundary: "agent-command.local" } as never }),
],
[
"ingress boundary",
"boundary",
"agent-command.local",
() => facts({ ingress: { kind: "local-cli" } as never }),
],
["invoker state", "state", "unknown", () => facts({ invoker: {} as never })],
[
"invoker kind",
"kind",
"local-account",
() => facts({ invoker: { state: "present", rawPrincipalRef: "owned" } as never }),
],
[
"invoker principal",
"rawPrincipalRef",
"inherited-principal",
() => facts({ invoker: { state: "present", kind: "local-account" } as never }),
],
[
"grant reference",
"rawGrantRef",
"inherited-grant",
() => facts({ applicableGrants: [{ state: "present" } as never] }),
],
[
"grant state",
"state",
"present",
() => facts({ applicableGrants: [{ rawGrantRef: "owned-grant" } as never] }),
],
[
"assurance kind",
"kind",
"other",
() =>
facts({
assurance: [{ rawEvidenceRef: "owned-evidence", strength: "self-asserted" } as never],
}),
],
[
"assurance reference",
"rawEvidenceRef",
"inherited-evidence",
() => facts({ assurance: [{ kind: "other", strength: "self-asserted" } as never] }),
],
[
"assurance strength",
"strength",
"self-asserted",
() => facts({ assurance: [{ kind: "other", rawEvidenceRef: "owned-evidence" } as never] }),
],
] as const)(
"rejects inherited required $0 before allocation and enqueue",
(_name, key, inheritedValue, admissionFacts) => {
const prior = Object.getOwnPropertyDescriptor(Object.prototype, key);
let inheritedReads = 0;
let allocationReads = 0;
const sink = vi.fn(() => true);
const clear = configureExecutionIdentityAdmissionSink(sink);
try {
defineObjectPrototypeProperty(key, {
configurable: true,
enumerable: false,
get: () => {
inheritedReads += 1;
return inheritedValue;
},
});
const options = { enabled: true, runtimeInstanceId: "runtime-owned" };
Object.defineProperty(options, "contextId", {
enumerable: true,
get: () => {
allocationReads += 1;
return "must-not-allocate";
},
});
expect(
enqueueExecutionIdentityContextAtAdmission(admissionFacts() as never, options),
).toBeUndefined();
} finally {
clear();
restoreObjectPrototypeProperty(key, prior);
}
expect(inheritedReads).toBe(0);
expect(allocationReads).toBe(0);
expect(sink).not.toHaveBeenCalled();
},
);
it.each([
{
name: "outer ingress",
prepare: () => {
const admissionFacts = facts();
return { admissionFacts, target: admissionFacts, key: "ingress" };
},
},
...["invoker", "applicableGrants", "assurance"].map((key) => ({
name: `outer ${key}`,
prepare: () => {
const admissionFacts = facts();
return { admissionFacts, target: admissionFacts, key };
},
})),
...["kind", "boundary", "state", "rawSourceRef"].map((key) => ({
name: `ingress ${key}`,
prepare: () => {
const admissionFacts = facts();
return { admissionFacts, target: admissionFacts.ingress, key };
},
})),
...["state", "kind", "rawPrincipalRef", "displayLabel"].map((key) => ({
name: `invoker ${key}`,
prepare: () => {
const invoker = {
state: "present" as const,
kind: "local-account" as const,
rawPrincipalRef: "owned-principal",
displayLabel: "owned-label",
};
const admissionFacts = facts({ invoker });
return { admissionFacts, target: invoker, key };
},
})),
...["rawGrantRef", "state"].map((key) => ({
name: `grant ${key}`,
prepare: () => {
const grant = { rawGrantRef: "owned-grant", state: "present" as const };
const admissionFacts = facts({ applicableGrants: [grant] });
return { admissionFacts, target: grant, key };
},
})),
...["kind", "rawEvidenceRef", "strength"].map((key) => ({
name: `assurance ${key}`,
prepare: () => {
const assurance = {
kind: "other" as const,
rawEvidenceRef: "owned-evidence",
strength: "self-asserted" as const,
};
const admissionFacts = facts({ assurance: [assurance] });
return { admissionFacts, target: assurance, key };
},
})),
])("rejects an own accessor at $name without reading it or allocating", ({ prepare }) => {
const { admissionFacts, target, key } = prepare();
let accessorReads = 0;
let allocationReads = 0;
Object.defineProperty(target, key, {
configurable: true,
enumerable: true,
get: () => {
accessorReads += 1;
return "must-not-read";
},
});
const options = { enabled: true, runtimeInstanceId: "runtime-owned" };
Object.defineProperty(options, "contextId", {
enumerable: true,
get: () => {
allocationReads += 1;
return "must-not-allocate";
},
});
const sink = vi.fn(() => true);
const clear = configureExecutionIdentityAdmissionSink(sink);
try {
expect(
enqueueExecutionIdentityContextAtAdmission(admissionFacts as never, options),
).toBeUndefined();
} finally {
clear();
}
expect(accessorReads).toBe(0);
expect(allocationReads).toBe(0);
expect(sink).not.toHaveBeenCalled();
});
it("rejects malformed, ambiguous, oversized, and noncanonical invoker variants", () => {
const present = captureEnvelope(
facts({
@@ -271,6 +661,65 @@ describe("execution identity admission envelope", () => {
expect(accessorReads).toBe(0);
});
it("revalidates envelopes and worker messages from owned data only", () => {
const envelope = captureEnvelope(facts(), {
contextId: "context-revalidation",
executionId: "execution-revalidation",
now: 1,
runtimeInstanceId: "runtime-owned",
});
const priorInvoker = Object.getOwnPropertyDescriptor(Object.prototype, "invoker");
const priorIngress = Object.getOwnPropertyDescriptor(Object.prototype, "ingress");
const priorKind = Object.getOwnPropertyDescriptor(Object.prototype, "kind");
let inheritedReads = 0;
let parsed: ExecutionIdentityAdmissionEnvelope;
try {
defineObjectPrototypeProperty("invoker", {
configurable: true,
enumerable: false,
get: () => {
inheritedReads += 1;
return { state: "unknown" };
},
});
parsed = parseExecutionIdentityAdmissionEnvelope(envelope);
defineObjectPrototypeProperty("ingress", {
configurable: true,
enumerable: false,
get: () => {
inheritedReads += 1;
return envelope.ingress;
},
});
expect(() => parseExecutionIdentityAdmissionEnvelope(omitOwn(envelope, "ingress"))).toThrow(
"execution identity admission envelope violates its bounded contract",
);
defineObjectPrototypeProperty("kind", {
configurable: true,
enumerable: false,
get: () => {
inheritedReads += 1;
return "capture";
},
});
expect(() => parseExecutionIdentityAdmissionWork({ envelope } as never)).toThrow(
"execution identity admission work violates its bounded contract",
);
} finally {
for (const [key, descriptor] of [
["invoker", priorInvoker],
["ingress", priorIngress],
["kind", priorKind],
] as const) {
restoreObjectPrototypeProperty(key, descriptor);
}
}
expect(inheritedReads).toBe(0);
expect(Object.hasOwn(parsed!, "invoker")).toBe(false);
});
it("rejects invalid owned facts, excess items, and oversized encoded envelopes", () => {
expect(() =>
captureEnvelope(facts({ runId: "" }), {
@@ -394,3 +843,9 @@ describe("execution identity admission envelope", () => {
expect(JSON.stringify(work.mock.calls)).not.toContain("raw-private-reference");
});
});
function omitOwn<T extends object, K extends keyof T>(value: T, key: K): Omit<T, K> {
const copy = { ...value };
delete copy[key];
return copy;
}
+137 -108
View File
@@ -25,6 +25,58 @@ const evidenceState = () =>
const closedObject = <T extends Parameters<typeof Type.Object>[0]>(properties: T) =>
Type.Object(properties, { additionalProperties: false });
const ingressKind = () =>
Type.Union([
Type.Literal("local-cli"),
Type.Literal("gateway-client"),
Type.Literal("channel"),
Type.Literal("api"),
Type.Literal("schedule"),
Type.Literal("webhook"),
Type.Literal("task"),
Type.Literal("subagent"),
Type.Literal("acp"),
Type.Literal("worker"),
Type.Literal("plugin"),
Type.Literal("recovery"),
Type.Literal("system"),
]);
const runtimeKind = () =>
Type.Union([
Type.Literal("gateway"),
Type.Literal("embedded"),
Type.Literal("worker"),
Type.Literal("plugin-harness"),
Type.Literal("acp"),
]);
const admissionGrant = () => closedObject({ rawGrantRef: rawRef(), state: evidenceState() });
const admissionGrants = () =>
Type.Array(admissionGrant(), { maxItems: EXECUTION_IDENTITY_ADMISSION_MAX_ITEMS });
const admissionAssurance = () =>
Type.Array(
closedObject({
kind: Type.Union([
Type.Literal("durable-profile"),
Type.Literal("trusted-proxy"),
Type.Literal("tailscale-whois"),
Type.Literal("device-proof"),
Type.Literal("channel-admission"),
Type.Literal("local-process"),
Type.Literal("spawn-lineage"),
Type.Literal("worker-admission"),
Type.Literal("runtime-binding"),
Type.Literal("other"),
]),
rawEvidenceRef: rawRef(),
strength: Type.Union([
Type.Literal("self-asserted"),
Type.Literal("boundary-verified"),
Type.Literal("cryptographic"),
]),
}),
{ maxItems: EXECUTION_IDENTITY_ADMISSION_MAX_ITEMS },
);
const ExecutionIdentityAdmissionInvokerSchema = Type.Union([
closedObject({
state: Type.Literal("present"),
@@ -53,61 +105,32 @@ const ExecutionIdentityAdmissionEnvelopeSchema = closedObject({
runtimeInstanceId: rawRef(),
agentId: boundedRef(),
ingress: closedObject({
kind: Type.Union([
Type.Literal("local-cli"),
Type.Literal("gateway-client"),
Type.Literal("channel"),
Type.Literal("api"),
Type.Literal("schedule"),
Type.Literal("webhook"),
Type.Literal("task"),
Type.Literal("subagent"),
Type.Literal("acp"),
Type.Literal("worker"),
Type.Literal("plugin"),
Type.Literal("recovery"),
Type.Literal("system"),
]),
kind: ingressKind(),
boundary: boundedRef(),
state: evidenceState(),
rawSourceRef: Type.Optional(rawRef()),
}),
runtime: closedObject({
kind: Type.Union([
Type.Literal("gateway"),
Type.Literal("embedded"),
Type.Literal("worker"),
Type.Literal("plugin-harness"),
Type.Literal("acp"),
]),
kind: runtimeKind(),
}),
invoker: Type.Optional(ExecutionIdentityAdmissionInvokerSchema),
applicableGrants: Type.Array(closedObject({ rawGrantRef: rawRef(), state: evidenceState() }), {
maxItems: EXECUTION_IDENTITY_ADMISSION_MAX_ITEMS,
applicableGrants: admissionGrants(),
assurance: admissionAssurance(),
});
const ExecutionIdentityAdmissionFactsSchema = closedObject({
runId: boundedRef(),
agentId: boundedRef(),
ingress: closedObject({
kind: ingressKind(),
boundary: boundedRef(),
state: Type.Optional(evidenceState()),
rawSourceRef: Type.Optional(rawRef()),
}),
assurance: Type.Array(
closedObject({
kind: Type.Union([
Type.Literal("durable-profile"),
Type.Literal("trusted-proxy"),
Type.Literal("tailscale-whois"),
Type.Literal("device-proof"),
Type.Literal("channel-admission"),
Type.Literal("local-process"),
Type.Literal("spawn-lineage"),
Type.Literal("worker-admission"),
Type.Literal("runtime-binding"),
Type.Literal("other"),
]),
rawEvidenceRef: rawRef(),
strength: Type.Union([
Type.Literal("self-asserted"),
Type.Literal("boundary-verified"),
Type.Literal("cryptographic"),
]),
}),
{ maxItems: EXECUTION_IDENTITY_ADMISSION_MAX_ITEMS },
),
runtime: closedObject({ kind: runtimeKind() }),
invoker: Type.Optional(ExecutionIdentityAdmissionInvokerSchema),
applicableGrants: Type.Optional(admissionGrants()),
assurance: Type.Optional(admissionAssurance()),
});
const ExecutionIdentityAdmissionTokenSchema = closedObject({
@@ -166,9 +189,11 @@ function freezeEnvelope<T>(value: T, seen = new WeakSet<object>()): T {
return Object.freeze(value);
}
function assertPlainCloneData(value: unknown, ancestors = new WeakSet<object>()): void {
// Snapshot descriptors before schema or projection: TypeBox accepts inherited
// keys, which would otherwise turn prototype data into diagnostic provenance.
function copyOwnedData<T>(value: T, ancestors = new WeakSet<object>()): T {
if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
return;
return value;
}
if (typeof value !== "object" || isProxy(value)) {
throw new Error("execution identity admission data must be clone-safe plain data");
@@ -180,10 +205,15 @@ function assertPlainCloneData(value: unknown, ancestors = new WeakSet<object>())
try {
const prototype = Object.getPrototypeOf(value);
const keys = Reflect.ownKeys(value);
const array = Array.isArray(value);
if (Array.isArray(value)) {
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
if (
prototype !== Array.prototype ||
keys.length !== value.length + 1 ||
!lengthDescriptor ||
!("value" in lengthDescriptor) ||
typeof lengthDescriptor.value !== "number" ||
keys.length !== lengthDescriptor.value + 1 ||
keys.at(-1) !== "length"
) {
throw new Error("execution identity admission data must be clone-safe plain data");
@@ -191,51 +221,63 @@ function assertPlainCloneData(value: unknown, ancestors = new WeakSet<object>())
} else if (prototype !== Object.prototype && prototype !== null) {
throw new Error("execution identity admission data must be clone-safe plain data");
}
const copy: unknown[] | Record<string, unknown> = array ? [] : Object.create(null);
for (const [index, key] of keys.entries()) {
if (key === "length" && Array.isArray(value)) {
if (key === "length" && array) {
continue;
}
if (typeof key !== "string" || (Array.isArray(value) && key !== String(index))) {
if (typeof key !== "string" || (array && key !== String(index))) {
throw new Error("execution identity admission data must be clone-safe plain data");
}
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (!descriptor?.enumerable || !("value" in descriptor)) {
throw new Error("execution identity admission data must be clone-safe plain data");
}
assertPlainCloneData(descriptor.value, ancestors);
Object.defineProperty(copy, key, {
configurable: true,
enumerable: true,
value: copyOwnedData(descriptor.value, ancestors),
writable: true,
});
}
return copy as T;
} finally {
ancestors.delete(value);
}
}
function validateEnvelope(value: unknown): asserts value is ExecutionIdentityAdmissionEnvelope {
assertPlainCloneData(value);
function validateEnvelope(value: unknown): ExecutionIdentityAdmissionEnvelope {
const owned = copyOwnedData(value);
if (
!Value.Check(ExecutionIdentityAdmissionEnvelopeSchema, value) ||
!Number.isSafeInteger(value.createdAt)
!Value.Check(ExecutionIdentityAdmissionEnvelopeSchema, owned) ||
!Number.isSafeInteger(owned.createdAt)
) {
throw new Error("execution identity admission envelope violates its bounded contract");
}
const encoded = JSON.stringify(value);
const encoded = JSON.stringify(owned);
if (Buffer.byteLength(encoded, "utf8") > EXECUTION_IDENTITY_ADMISSION_MAX_BYTES) {
throw new Error("execution identity admission envelope exceeds 16 KiB");
}
return owned;
}
function validateRawInvoker(value: unknown): void {
if (value !== undefined && !Value.Check(ExecutionIdentityAdmissionInvokerSchema, value)) {
throw new Error("execution identity admission invoker violates its bounded contract");
function validateFacts(value: unknown): ExecutionIdentityAdmissionFacts {
const owned = copyOwnedData(value);
if (!Value.Check(ExecutionIdentityAdmissionFactsSchema, owned)) {
throw new Error("execution identity admission facts violate their bounded contract");
}
return owned;
}
function validateToken(value: unknown): asserts value is ExecutionIdentityAdmissionToken {
function validateToken(value: unknown): ExecutionIdentityAdmissionToken {
const owned = copyOwnedData(value);
if (
!Value.Check(ExecutionIdentityAdmissionTokenSchema, value) ||
!Number.isSafeInteger(value.createdAt)
!Value.Check(ExecutionIdentityAdmissionTokenSchema, owned) ||
!Number.isSafeInteger(owned.createdAt)
) {
throw new Error("execution identity admission token violates its bounded contract");
}
return owned;
}
/** Allocate the immutable correlation owned by one outer admitted turn. */
@@ -250,15 +292,13 @@ export function createExecutionIdentityAdmissionToken(
runId,
createdAt: options.now ?? Date.now(),
};
validateToken(token);
return freezeEnvelope(token);
return freezeEnvelope(validateToken(token));
}
export function parseExecutionIdentityAdmissionToken(
value: unknown,
): ExecutionIdentityAdmissionToken {
validateToken(value);
return freezeEnvelope({ ...value });
return freezeEnvelope(validateToken(value));
}
function redactDisplayLabel(value: string): string {
@@ -274,22 +314,12 @@ function redactDisplayLabel(value: string): string {
function captureExecutionIdentityAdmissionEnvelope(
facts: ExecutionIdentityAdmissionFacts,
options: {
contextId?: string;
executionId?: string;
now?: number;
runtimeInstanceId?: string;
token?: ExecutionIdentityAdmissionToken;
} = {},
token: ExecutionIdentityAdmissionToken;
},
): ExecutionIdentityAdmissionEnvelope {
const token =
options.token ??
createExecutionIdentityAdmissionToken(facts.runId, {
contextId: options.contextId,
executionId: options.executionId,
now: options.now,
});
validateToken(token);
if (token.runId !== facts.runId) {
const ownedToken = validateToken(options.token);
if (ownedToken.runId !== facts.runId) {
throw new Error("execution identity admission token disagrees with the admitted run");
}
const runtimeInstanceId = options.runtimeInstanceId ?? PROCESS_RUNTIME_INSTANCE_ID;
@@ -302,10 +332,10 @@ function captureExecutionIdentityAdmissionEnvelope(
];
const envelope = {
envelopeVersion: 1 as const,
contextId: token.contextId,
executionId: token.executionId,
runId: token.runId,
createdAt: token.createdAt,
contextId: ownedToken.contextId,
executionId: ownedToken.executionId,
runId: ownedToken.runId,
createdAt: ownedToken.createdAt,
runtimeInstanceId,
agentId: facts.agentId,
ingress: { ...facts.ingress, state: facts.ingress.state ?? "present" },
@@ -337,24 +367,23 @@ function captureExecutionIdentityAdmissionEnvelope(
strength: item.strength,
})),
};
validateEnvelope(envelope);
return freezeEnvelope(envelope);
return freezeEnvelope(validateEnvelope(envelope));
}
/** Revalidate a structured-cloned worker message before any persistence work. */
export function parseExecutionIdentityAdmissionEnvelope(
value: unknown,
): ExecutionIdentityAdmissionEnvelope {
validateEnvelope(value);
const parsed = captureExecutionIdentityAdmissionEnvelope(value, {
token: createExecutionIdentityAdmissionToken(value.runId, {
contextId: value.contextId,
executionId: value.executionId,
now: value.createdAt,
const envelope = validateEnvelope(value);
const parsed = captureExecutionIdentityAdmissionEnvelope(envelope, {
token: createExecutionIdentityAdmissionToken(envelope.runId, {
contextId: envelope.contextId,
executionId: envelope.executionId,
now: envelope.createdAt,
}),
runtimeInstanceId: value.runtimeInstanceId,
runtimeInstanceId: envelope.runtimeInstanceId,
});
if (JSON.stringify(parsed) !== JSON.stringify(value)) {
if (JSON.stringify(parsed) !== JSON.stringify(envelope)) {
throw new Error("execution identity admission envelope is not canonical");
}
return parsed;
@@ -364,10 +393,11 @@ export function parseExecutionIdentityAdmissionEnvelope(
export function parseExecutionIdentityAdmissionWork(
value: unknown,
): ExecutionIdentityAdmissionWork {
if (!value || typeof value !== "object") {
const owned = copyOwnedData(value);
if (!owned || typeof owned !== "object") {
throw new Error("execution identity admission work violates its bounded contract");
}
const work = value as { kind?: unknown; envelope?: unknown; token?: unknown };
const work = owned as { kind?: unknown; envelope?: unknown; token?: unknown };
if (work.kind === "capture") {
return freezeEnvelope({
kind: "capture" as const,
@@ -424,21 +454,20 @@ export function enqueueExecutionIdentityContextAtAdmission(
return undefined;
}
try {
assertPlainCloneData(facts);
validateRawInvoker(facts.invoker);
const token =
const ownedFacts = validateFacts(facts);
const token = validateToken(
options.token ??
createExecutionIdentityAdmissionToken(facts.runId, {
contextId: options.contextId,
executionId: options.executionId,
now: options.now,
});
validateToken(token);
createExecutionIdentityAdmissionToken(ownedFacts.runId, {
contextId: options.contextId,
executionId: options.executionId,
now: options.now,
}),
);
const work: ExecutionIdentityAdmissionWork = options.retryOnly
? { kind: "retry-reference", token }
: {
kind: "capture",
envelope: captureExecutionIdentityAdmissionEnvelope(facts, {
envelope: captureExecutionIdentityAdmissionEnvelope(ownedFacts, {
token,
runtimeInstanceId: options.runtimeInstanceId,
}),