Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/quiet-runs-bind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tangle-network/agent-interface": minor
"@tangle-network/agent-provider-tangle": minor
---

Expose detached environment metadata and preserve a recursively frozen Sandbox metadata snapshot in the Tangle provider.
2 changes: 2 additions & 0 deletions packages/agent-interface/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ shapes; higher-level packages import from here rather than redefining them.
`AgentRunControlRef` identifies a retained run without depending on a live JavaScript object and may carry the provider's admission digest so reconstruction can reject changed-input reuse.
`RuntimeEventEnvelope` adds stable run, event, sequence, cursor, and timestamp fields around the existing `StreamEvent` union, and its runtime schema validates every canonical event variant.
Providers advertise `retainedControl` only when exact run, result, event, cancellation, replay, detach, turn, and session identity are all implemented together.
`AgentEnvironment.metadata` is the detached snapshot returned by create or get, so recovery can check persisted annotations without listing environments.
Metadata can include caller-authored values and does not prove authorization or authorship.
`AgentSession.cancelRun()` accepts a canonical request digest bound to one operation and `AgentExactRunControlRef`, so a caller can safely repeat the same cancellation after losing the first acknowledgement.
Its acknowledgement repeats the operation, digest, and run coordinates and distinguishes a known cancellation effect from conflict or unknown state.

Expand Down
5 changes: 5 additions & 0 deletions packages/agent-interface/src/environment-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ export interface AgentEnvironment {
readonly id: string;
readonly provider: string;
readonly name?: string;
/**
* Detached metadata returned by the provider.
* It can contain caller-authored annotations and is not authorization evidence.
*/
readonly metadata?: Readonly<Record<string, unknown>>;
/**
* The capability document for THIS environment, and the document a caller
* reads to decide which operation to offer against it.
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-provider-tangle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const provider = createTangleProvider({

Detached dispatch returns the immutable Sandbox execution receipt in `controlRef`.
The adapter validates its complete capability document and omits optional environment methods whose capabilities are disabled.
Created and reconstructed environments expose a recursively frozen Sandbox metadata snapshot for constant-time annotation checks.
Sandbox metadata can include caller-authored values and does not authenticate its author.
Reconstruct an exact session with `environment.session(reference.id, { controlRef: reference.controlRef })`; replay cursors are exclusive at both the agent interface and Sandbox session stream.
Result, replay, and cancel operations select that exact execution instead of whichever execution most recently changed the shared session.
Session status with an exact control reference reports a state only when the payload names that execution; a payload bound to a different or unnamed execution reports `unknown`.
Expand Down
22 changes: 22 additions & 0 deletions packages/agent-provider-tangle/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ describe("createTangleProvider", () => {
id: "sbx-1",
name: "sandbox-one",
status: "running",
metadata: {
retainedIdempotencyKey: "environment-key",
labels: { tenant: "acme" },
},
async *streamPrompt(prompt: string): AsyncIterable<SandboxEvent> {
yield {
type: "result",
Expand Down Expand Up @@ -399,6 +403,9 @@ describe("createTangleProvider", () => {
createRequestOptions = requestOptions;
return box;
},
async get(id) {
return id === box.id ? box : null;
},
describePlacement: () => ({ kind: "sibling", sandboxId: "sbx-1" }),
};
const provider = createTangleProvider({ client });
Expand All @@ -421,6 +428,21 @@ describe("createTangleProvider", () => {
backend: { type: "codex", profile: { name: "worker" } },
});
expect(createRequestOptions?.signal).toBe(controller.signal);
const environment = await provider.create({ profile: { name: "worker" } });
expect(environment.metadata).toEqual({
retainedIdempotencyKey: "environment-key",
labels: { tenant: "acme" },
});
expect(Object.isFrozen(environment.metadata)).toBe(true);
expect(Object.isFrozen(environment.metadata?.labels)).toBe(true);
(box.metadata?.labels as { tenant: string }).tenant = "changed-at-source";
expect(environment.metadata?.labels).toEqual({ tenant: "acme" });

const reconstructed = await provider.get?.("sbx-1");
expect(reconstructed?.metadata?.labels).toEqual({ tenant: "changed-at-source" });
expect(Object.isFrozen(reconstructed?.metadata?.labels)).toBe(true);
(box.metadata?.labels as { tenant: string }).tenant = "changed-after-reconstruction";
expect(reconstructed?.metadata?.labels).toEqual({ tenant: "changed-at-source" });
});

it("rejects malformed Sandbox events and exec results", async () => {
Expand Down
14 changes: 14 additions & 0 deletions packages/agent-provider-tangle/src/tangle-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export async function sandboxInstanceAsEnvironment(
id: environmentId,
provider: providerName,
...(box.name ? { name: boundedString(box.name, "Tangle environment name") } : {}),
...(box.metadata ? { metadata: snapshotMetadata(box.metadata) } : {}),
capabilities,
async status(options?: { signal?: AbortSignal }): Promise<AgentEnvironmentStatus> {
assertOptionKeys(options, ["signal"], "Tangle environment status");
Expand Down Expand Up @@ -292,3 +293,16 @@ export async function sandboxInstanceAsEnvironment(
: {}),
};
}

function snapshotMetadata(
metadata: Record<string, unknown>,
): Readonly<Record<string, unknown>> {
return deepFreeze(structuredClone(metadata));
}

function deepFreeze<T>(value: T, seen = new Set<object>()): T {
if (value === null || typeof value !== "object" || seen.has(value)) return value;
seen.add(value);
for (const child of Object.values(value)) deepFreeze(child, seen);
return Object.freeze(value);
}
Loading