mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user