Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/typescript/src/agt-approval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@ function verifyPairs(left: Source, right: Source, pairs: Array<[string, string]>
function digest(value: unknown, field: string): Digest { const match = DIGEST.exec(required(value, field)); if (!match) throw new Error(`AGT ${field} must use sha256:<lowercase-hex>`); return {algorithm: "sha256", value: match[1]!}; }
function required(value: unknown, field: string): string { if (typeof value !== "string" || !value) throw new Error(`AGT ${field} must be a non-empty string`); return value; }
function context(value: {traceId?: string; spanId?: string}): {traceId?: string; spanId?: string} { return {...(value.traceId === undefined ? {} : {traceId: required(value.traceId, "traceId")}), ...(value.spanId === undefined ? {} : {spanId: required(value.spanId, "spanId")})}; }
function timestampNs(value: unknown, field: string): bigint { if (typeof value !== "string") throw new Error(`AGT ${field} must be an RFC 3339 UTC timestamp string`); const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(?:Z|\+00:00)$/.exec(value); if (!match) throw new Error(`AGT ${field} must be an RFC 3339 UTC timestamp string`); const milliseconds = Date.parse(`${match[1]}Z`); if (match[1]!.startsWith("0000") || !Number.isFinite(milliseconds) || new Date(milliseconds).toISOString().slice(0, 19) !== match[1]) throw new Error(`AGT ${field} is not a valid timestamp`); return BigInt(milliseconds) * 1_000_000n + BigInt((match[2] ?? "").padEnd(9, "0") || "0"); }
function timestampNs(value: unknown, field: string): bigint { if (typeof value !== "string") throw new Error(`AGT ${field} must be an RFC 3339 UTC timestamp string`); const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(?:Z|\+00:00)$/.exec(value); if (!match) throw new Error(`AGT ${field} must be an RFC 3339 UTC timestamp string`); const milliseconds = Date.parse(`${match[1]}Z`); if (match[1]!.startsWith("0000") || !Number.isFinite(milliseconds) || milliseconds < 0 || new Date(milliseconds).toISOString().slice(0, 19) !== match[1]) throw new Error(`AGT ${field} is not a valid timestamp`); return BigInt(milliseconds) * 1_000_000n + BigInt((match[2] ?? "").padEnd(9, "0") || "0"); }
function sourceEventId(kind: string, id: string): string { const ns = Buffer.from(NAMESPACE.replaceAll("-", ""), "hex"); const bytes = createHash("sha1").update(ns).update(`${kind}:${id}`, "utf8").digest().subarray(0, 16); bytes[6] = (bytes[6]! & 0x0f) | 0x50; bytes[8] = (bytes[8]! & 0x3f) | 0x80; const hex = bytes.toString("hex"); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; }
2 changes: 1 addition & 1 deletion packages/typescript/src/agt-audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ function actionOutcome(value: unknown): string { const result = new Map<unknown,
function auditDuration(source: Source): number { if (source.issued_at === undefined || source.issued_at === null || source.completed_at === undefined || source.completed_at === null) throw new Error("AGT action audit requires durationNs or both issued_at and completed_at"); const start = timestampNs(source.issued_at, "issued_at"); const end = timestampNs(source.completed_at, "completed_at"); if (end < start) throw new Error("AGT completed_at cannot predate issued_at"); const value = end - start; if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("AGT audit duration exceeds the safe integer range"); return Number(value); }
function safeDuration(value: number, field: string): void { if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${field} must be a non-negative safe integer`); }
function required(value: unknown, field: string): string { if (typeof value !== "string" || !value) throw new Error(`AGT audit ${field} must be a non-empty string`); return value; }
function timestampNs(value: unknown, field: string): bigint { if (typeof value !== "string") throw new Error(`AGT audit ${field} must be an RFC 3339 UTC timestamp`); const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(?:Z|\+00:00)$/.exec(value); if (!match) throw new Error(`AGT audit ${field} must be an RFC 3339 UTC timestamp`); const milliseconds = Date.parse(`${match[1]}Z`); if (match[1]!.startsWith("0000") || !Number.isFinite(milliseconds) || new Date(milliseconds).toISOString().slice(0, 19) !== match[1]) throw new Error(`AGT audit ${field} is invalid`); return BigInt(milliseconds) * 1_000_000n + BigInt((match[2] ?? "").padEnd(9, "0") || "0"); }
function timestampNs(value: unknown, field: string): bigint { if (typeof value !== "string") throw new Error(`AGT audit ${field} must be an RFC 3339 UTC timestamp`); const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(?:Z|\+00:00)$/.exec(value); if (!match) throw new Error(`AGT audit ${field} must be an RFC 3339 UTC timestamp`); const milliseconds = Date.parse(`${match[1]}Z`); if (match[1]!.startsWith("0000") || !Number.isFinite(milliseconds) || milliseconds < 0 || new Date(milliseconds).toISOString().slice(0, 19) !== match[1]) throw new Error(`AGT audit ${field} is invalid`); return BigInt(milliseconds) * 1_000_000n + BigInt((match[2] ?? "").padEnd(9, "0") || "0"); }
function sourceEventId(kind: string, id: string): string { const ns = Buffer.from(NAMESPACE.replaceAll("-", ""), "hex"); const bytes = createHash("sha1").update(ns).update(`${kind}:${id}`, "utf8").digest().subarray(0, 16); bytes[6] = (bytes[6]! & 0x0f) | 0x50; bytes[8] = (bytes[8]! & 0x3f) | 0x80; const hex = bytes.toString("hex"); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; }
2 changes: 1 addition & 1 deletion packages/typescript/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class EventFactory {
readonly clockNs: () => bigint = () => BigInt(Date.now()) * 1_000_000n,
readonly eventIdFactory: () => string = randomUUID,
) {
this.#producer = {name: producer.name, version: producer.version, ...(producer.instanceId ? {instance_id: producer.instanceId} : {})};
this.#producer = {name: producer.name, version: producer.version, ...(producer.instanceId === undefined ? {} : {instance_id: producer.instanceId})};
}

build(eventType: string, fields: EnvelopeFields, payload: Record<string, unknown>): NormalizedEvent {
Expand Down
1 change: 1 addition & 0 deletions packages/typescript/test/agt-approval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ test("resolution mapping rejects request mismatch, chronology reversal, and malf
test("only terminal outcomes map to terminal approval events", () => {
for (const [outcome, expected] of [["allow", "approval.approved"], ["deny", "approval.rejected"], ["expired", "approval.expired"]]) assert.equal(agtApprovalResolution(factory, {...resolution, outcome}, request, {runId: "run-1"}).event_type, expected);
assert.throws(() => agtApprovalResolution(factory, {...resolution, outcome: "vote"}, request, {runId: "run-1"}), /unsupported/);
assert.throws(() => agtPolicyDecisionRecord(factory, {...policy, decided_at: "1969-12-31T23:59:59Z"}, {runId: "run-1", agentId: "agent-1", actionType: "deploy", resourceType: "environment", policyEngineVersion: "1.0.0", bundleDigest: bundle}), /not a valid timestamp/);
});
3 changes: 3 additions & 0 deletions packages/typescript/test/agt-audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ test("audit action rejects absent, reversed, and unsafe timing", () => {
assert.throws(() => agtAuditAction(factory, source({event_type: "tool_invocation", issued_at: null}), {runId: "run-1", actionDigest: digest, actionKind: "tool", operation: "invoke"}), /requires durationNs/);
assert.throws(() => agtAuditAction(factory, source({event_type: "tool_invocation", issued_at: "2026-08-18T12:00:01Z"}), {runId: "run-1", actionDigest: digest, actionKind: "tool", operation: "invoke"}), /cannot predate/);
assert.throws(() => agtAuditAction(factory, source({event_type: "tool_invocation"}), {runId: "run-1", actionDigest: digest, actionKind: "tool", operation: "invoke", durationNs: Number.MAX_SAFE_INTEGER + 1}), /safe integer/);
assert.throws(() => agtAuditPolicyDecision(factory, source({timestamp: "1969-12-31T23:59:59Z"}), {runId: "run-1", policyEngineVersion: "5.0.0", bundleDigest: bundle, resourceType: "tool"}), /timestamp is invalid/);
const emptyInstFactory = new EventFactory(SchemaValidator.bundled(), {name: "audit-tests", version: "1", instanceId: ""});
assert.throws(() => emptyInstFactory.build("usage.recorded", {runId: "run-1", agentId: "agent-1"}, {scope: "agent_run", operation: "build", input_tokens: 1}));
});
4 changes: 2 additions & 2 deletions src/agentrust_telemetry/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(
self._producer = {
"name": producer_name,
"version": producer_version,
**({"instance_id": producer_instance_id} if producer_instance_id else {}),
**({"instance_id": producer_instance_id} if producer_instance_id is not None else {}),
}
self._clock_ns = clock_ns
self._event_id_factory = event_id_factory
Expand Down Expand Up @@ -56,7 +56,7 @@ def build(
timestamp = self._clock_ns() if time_unix_nano is None else time_unix_nano
event: dict[str, Any] = {
"spec_version": "0.1.0-alpha.6",
"event_id": event_id or str(self._event_id_factory()),
"event_id": str(self._event_id_factory()) if event_id is None else event_id,
"event_type": event_type,
"time_unix_nano": _unix_nano(timestamp),
"run_id": run_id,
Expand Down
25 changes: 25 additions & 0 deletions tests/test_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@ def test_factory_prevents_envelope_override(self):
input_tokens=1,
producer={"name": "spoofed", "version": "9"},
)
with self.assertRaises(Exception):
self.factory.build(
"usage.recorded",
run_id="run-1",
agent_id="agent-1",
event_id="",
scope="agent_run",
operation="build",
input_tokens=1,
)
empty_inst = EventFactory(
SchemaValidator(ROOT / "spec" / "schema"),
producer_name="adapter-tests",
producer_version="1.0.0",
producer_instance_id="",
)
with self.assertRaises(Exception):
empty_inst.build(
"usage.recorded",
run_id="run-1",
agent_id="agent-1",
scope="agent_run",
operation="build",
input_tokens=1,
)

def test_factory_matches_cross_language_golden_event(self):
parity_factory = EventFactory(
Expand Down