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
524 changes: 518 additions & 6 deletions packages/cli/src/invoke-command.test.ts

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions packages/cli/src/invoke-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -974,10 +974,173 @@ export function renderHumanSummary(output: RunRecordV2 | RunRecordV2[]): string
const lines = [`preview: ${records.length} run(s) — ${records.length - failed} ok, ${failed} failed`];
for (const record of records) {
lines.push(` [${record.status === 'succeeded' ? 'ok' : 'FAIL'}] ${record.eventContract} ${record.runId} (${record.actions.length} action(s))`);
lines.push(
` policy: reads=${record.policy.reads} writes=${record.policy.writes} model=${record.policy.model} shell=${record.policy.shell} compose=${record.policy.compose}`
);
const fidelity = recordSourceFidelity(record);
if (fidelity) {
lines.push(` fidelity: ${Object.entries(fidelity).map(([key, value]) => `${key}=${value}`).join(' ')}`);
}
lines.push(' trace:');
if (record.actions.length === 0) {
lines.push(' (no effects)');
} else {
record.actions.forEach((action, index) => {
lines.push(...renderHumanAction(record, action, index + 1));
});
}
lines.push(...renderHumanStateChanges(record));
if (typeof record.error === 'string' && record.error) {
lines.push(` error: ${record.error}`);
}
}
return `${lines.join('\n')}\n`;
}

function renderHumanAction(
record: RunRecordV2,
action: RunRecordV2['actions'][number],
sequence: number
): string[] {
const data = asRecord(action.data) ?? {};
const prefix = ` ${String(sequence).padStart(2, '0')}.`;

if (action.kind === 'provider.write') {
return renderHumanProviderWrite(prefix, action, data);
}

if (action.kind === 'http.read' || action.kind === 'provider.read') {
const decision = action.status === 'denied' ? 'DENY' : 'ALLOW';
const fidelity = actionFidelity(record, action, data);
const provider = action.provider ? ` ${action.provider}${action.resource ? `.${action.resource}` : ''}` : '';
const method = typeof data.method === 'string' ? ` ${data.method}` : '';
const target = typeof data.url === 'string'
? ` ${data.url}`
: typeof data.path === 'string'
? ` ${data.path}`
: '';
const lines = [`${prefix} [${decision}] ${action.kind}${provider}${method}${target} fidelity=${fidelity}`];
const parameters = asRecord(data.parameters);
if (action.kind === 'provider.read' && parameters) {
lines.push(` parameters: ${JSON.stringify(parameters)}`);
}
return lines;
}

if (action.kind === 'model.complete') {
const mode = typeof data.mode === 'string' ? data.mode : record.policy.model;
const source = typeof data.source === 'string' ? data.source : 'unknown';
const fidelity = actionFidelity(record, action, data);
const sizes = [
typeof data.promptChars === 'number' ? `promptChars=${data.promptChars}` : '',
typeof data.outputChars === 'number' ? `outputChars=${data.outputChars}` : ''
].filter(Boolean).join(' ');
return [
`${prefix} [${humanActionStatus(action)}] model.complete mode=${mode} source=${source} fidelity=${fidelity}${sizes ? ` ${sizes}` : ''}`
];
}

const details = Object.keys(data).length > 0 ? ` ${JSON.stringify(data)}` : '';
return [`${prefix} [${humanActionStatus(action)}] ${action.kind}${details}`];
}

function renderHumanProviderWrite(
prefix: string,
action: RunRecordV2['actions'][number],
data: Record<string, unknown>
): string[] {
const provider = action.provider ?? 'unknown';
const resource = action.resource ?? 'unknown';
const method = typeof data.method === 'string' ? data.method : 'write';
const pathValue = typeof data.path === 'string' ? data.path : undefined;
const parameters = asRecord(data.parameters);
const body = asRecord(data.body);
const receipt = asRecord(data.simulatedReceipt);
const lines = [
`${prefix} [${humanActionStatus(action)}] provider.write ${provider}.${resource}`,
` method: ${method}`
];
if (pathValue) lines.push(` path: ${pathValue}`);
if (parameters) lines.push(` parameters: ${JSON.stringify(parameters)}`);

if (provider === 'slack') {
const channel = slackChannel(parameters, pathValue);
const parentRef = typeof body?.parentRef === 'string' ? body.parentRef : undefined;
const threadTs = typeof body?.thread_ts === 'string' ? body.thread_ts : undefined;
const isReply = resource === 'replies' || parentRef !== undefined || threadTs !== undefined;
lines.push(` slack message: ${isReply ? 'reply' : 'parent'}`);
lines.push(` channel: ${channel ?? '(unknown)'}`);
lines.push(
` linkage: parentRef=${parentRef ?? '(none)'} thread_ts=${threadTs ?? '(none)'} receipt=${typeof receipt?.id === 'string' ? receipt.id : '(none)'}`
);
if (typeof body?.text === 'string') lines.push(` text (exact): ${JSON.stringify(body.text)}`);
}

if (body) lines.push(` body: ${JSON.stringify(body)}`);
if (receipt) lines.push(` simulated receipt: ${JSON.stringify(receipt)}`);
lines.push(` provider data: ${JSON.stringify(data)}`);
return lines;
}

function humanActionStatus(action: RunRecordV2['actions'][number]): string {
if (action.status === 'denied') return 'DENIED';
if (action.status === 'previewed') return 'PREVIEW';
if (action.status === 'sandboxed') return 'SANDBOX';
return 'EXECUTED';
}

function recordSourceFidelity(record: RunRecordV2): Record<string, string> | undefined {
const extensions = asRecord(record.extensions);
const sourceFidelity = asRecord(extensions?.sourceFidelity);
if (!sourceFidelity) return undefined;
const entries = Object.entries(sourceFidelity).filter((entry): entry is [string, string] =>
typeof entry[1] === 'string'
);
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
}

function actionFidelity(
record: RunRecordV2,
action: RunRecordV2['actions'][number],
data: Record<string, unknown>
): string {
const extensions = asRecord(action.extensions);
if (typeof extensions?.sourceFidelity === 'string') return extensions.sourceFidelity;
if (typeof data.source === 'string') return data.source;
const sourceFidelity = recordSourceFidelity(record);
if (action.kind === 'http.read' && sourceFidelity?.http) return sourceFidelity.http;
if (action.kind === 'model.complete' && sourceFidelity?.model) return sourceFidelity.model;
if (action.kind === 'provider.read' && sourceFidelity?.state) return sourceFidelity.state;
if (action.status === 'denied') return 'unavailable';
return 'unknown';
}

function slackChannel(
parameters: Record<string, unknown> | null,
pathValue: string | undefined
): string | undefined {
const parameterChannel = parameters?.channelId ?? parameters?.channel;
if (typeof parameterChannel === 'string') return parameterChannel;
const match = pathValue?.match(/^\/slack\/channels\/([^/]+)/u);
return match?.[1] ? decodeURIComponent(match[1]) : undefined;
}

function renderHumanStateChanges(record: RunRecordV2): string[] {
const files = record.stateDiff.files ?? [];
const memory = record.stateDiff.memory ?? [];
const providers = record.stateDiff.providers ?? [];
if (files.length === 0 && memory.length === 0 && providers.length === 0) return [];

const lines = [' state changes:'];
for (const file of files) {
const change = file.before === undefined ? 'created' : file.after === undefined ? 'deleted' : 'updated';
lines.push(` file ${change}: ${file.path}`);
}
for (const entry of memory) lines.push(` memory: ${JSON.stringify(entry)}`);
for (const entry of providers) lines.push(` provider: ${JSON.stringify(entry)}`);
return lines;
}

function expectValue(flag: string, value: string | undefined): string {
if (value === undefined || value.startsWith('--')) throw new Error(`invoke: ${flag} expects a value`);
return value;
Expand Down
2 changes: 1 addition & 1 deletion packages/delivery/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
},
"dependencies": {
"@agentworkforce/runtime": "workspace:*",
"@relayfile/relay-helpers": "^0.4.6"
"@relayfile/relay-helpers": "^0.4.7"
}
}
21 changes: 21 additions & 0 deletions packages/events/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,24 @@ const contract = getEventContract(decoded.frame.type, decoded.frame.contractVers
Workforce gateway envelope. Legacy decoding is explicit in the returned
`compatibility` metadata. Known versioned frames reject unknown top-level
fields; forward-compatible data belongs in `extensions`.

Consumers that compile the exported contract schemas with AJV must install the
package-owned cross-field keywords before adding the schemas. These enforce
canonical coordinates that JSON Schema cannot express on its own, including
the URL-encoded Composio trigger resource path.

```ts
import addFormats from 'ajv-formats';
import { Ajv2020 } from 'ajv/dist/2020.js';
import {
addEventContractJsonSchemaKeywords,
EVENT_CONTRACT_JSON_SCHEMAS
} from '@agentworkforce/events';

const ajv = new Ajv2020({ strict: true });
addFormats(ajv);
addEventContractJsonSchemaKeywords(ajv);
for (const schema of Object.values(EVENT_CONTRACT_JSON_SCHEMAS)) {
ajv.addSchema(schema);
}
```
49 changes: 49 additions & 0 deletions packages/events/src/contract-schema-keywords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Ajv2020 } from 'ajv/dist/2020.js';

export const COMPOSIO_TRIGGER_COORDINATES_KEYWORD =
'x-agentworkforce-composio-trigger-message-v1-coordinates';

/**
* Install the shared cross-field keywords used by exported Event contract schemas.
*
* JSON Schema cannot otherwise compare a resource path with a URL-encoded value
* elsewhere in the frame. Consumers compiling EVENT_CONTRACT_JSON_SCHEMAS with
* AJV must call this once on their AJV instance before adding the schemas.
*/
export function addEventContractJsonSchemaKeywords(ajv: Ajv2020): Ajv2020 {
if (!ajv.getKeyword(COMPOSIO_TRIGGER_COORDINATES_KEYWORD)) {
ajv.addKeyword({
keyword: COMPOSIO_TRIGGER_COORDINATES_KEYWORD,
type: 'object',
schemaType: 'boolean',
errors: false,
validate(enabled: boolean, value: unknown): boolean {
if (!enabled) return true;
return hasCanonicalComposioTriggerCoordinates(value);
}
});
}
return ajv;
}

function hasCanonicalComposioTriggerCoordinates(value: unknown): boolean {
if (!isRecord(value) || !isRecord(value.resource) || !isRecord(value.delivery)) return false;
if (!isRecord(value.payload) || !isRecord(value.payload.metadata)) return false;

const triggerId = value.payload.metadata.trigger_id;
const payloadId = value.payload.id;
const timestamp = value.payload.timestamp;
if (typeof triggerId !== 'string' || triggerId.length === 0) return false;
if (typeof payloadId !== 'string' || payloadId.length === 0) return false;
if (typeof timestamp !== 'string' || timestamp.length === 0) return false;

return value.resource.id === triggerId
&& value.resource.path === `/composio/triggers/${encodeURIComponent(triggerId)}`
&& value.occurredAt === timestamp
&& value.delivery.id === payloadId
&& value.delivery.dedupeKey === payloadId;
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
4 changes: 4 additions & 0 deletions packages/events/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export {
EVENT_FRAME_V1_SCHEMA,
EVENT_SUMMARY_SCHEMA
} from './schemas.js';
export {
addEventContractJsonSchemaKeywords,
COMPOSIO_TRIGGER_COORDINATES_KEYWORD
} from './contract-schema-keywords.js';
export {
EVENT_CONTRACTS,
EVENT_CONTRACT_JSON_SCHEMAS,
Expand Down
2 changes: 2 additions & 0 deletions packages/events/src/json-schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import addFormatsModule from 'ajv-formats';
import { Ajv2020, type ErrorObject, type ValidateFunction } from 'ajv/dist/2020.js';
import { addEventContractJsonSchemaKeywords } from './contract-schema-keywords.js';
import type { JsonSchema, ValidationIssue, ValidationResult } from './types.js';

const ajv = new Ajv2020({ allErrors: true, strict: true });
(addFormatsModule as unknown as (instance: Ajv2020) => Ajv2020)(ajv);
addEventContractJsonSchemaKeywords(ajv);

const validators = new WeakMap<object, ValidateFunction>();

Expand Down
79 changes: 79 additions & 0 deletions packages/events/src/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import addFormatsModule from 'ajv-formats';
import { Ajv2020 } from 'ajv/dist/2020.js';
import type { EventFrameV1 } from './types.js';
import {
addEventContractJsonSchemaKeywords,
decodeEventFrame,
EVENT_CONTRACTS,
EVENT_CONTRACT_JSON_SCHEMAS,
Expand All @@ -19,6 +21,7 @@ test('every registry entry exports schema and validating example fixtures', () =
'github.pull_request.opened',
'slack.message.created',
'linear.issue.created',
'composio.trigger.message',
'relaycast.message'
]);
for (const contract of EVENT_CONTRACTS) {
Expand All @@ -31,9 +34,85 @@ test('every registry entry exports schema and validating example fixtures', () =
}
});

test('Composio V3 trigger contract requires its payload and canonical identity coordinates', () => {
const contract = EVENT_CONTRACTS.find((entry) => entry.id === 'composio.trigger.message');
assert.ok(contract);
const fixture = contract.fixtureExamples[0];
const payload = fixture.payload as Record<string, unknown>;
const metadata = payload.metadata as Record<string, unknown>;

assert.equal(contract.version, 1);
assert.equal(contract.provider, 'composio');
assert.equal(contract.trigger, 'trigger.message');
assert.equal(contract.resourceKind, 'composio.trigger');
assert.equal(fixture.resource.id, metadata.trigger_id);
assert.equal(fixture.resource.path, `/composio/triggers/${encodeURIComponent(String(metadata.trigger_id))}`);
assert.equal(fixture.occurredAt, payload.timestamp);
assert.equal(fixture.delivery?.id, payload.id);
assert.equal(fixture.delivery?.dedupeKey, payload.id);
assert.deepEqual(contract.validate(fixture), { valid: true, errors: [] });

const schema = EVENT_CONTRACT_JSON_SCHEMAS['composio.trigger.message@1'];
assert.ok(schema);
const ajv = new Ajv2020({ allErrors: true, strict: true });
(addFormatsModule as unknown as (instance: Ajv2020) => Ajv2020)(ajv);
addEventContractJsonSchemaKeywords(ajv);
const validateSchema = ajv.compile(schema);

const invalidFrames: EventFrameV1[] = [
{ ...fixture, payload: undefined },
{ ...fixture, payload: [] },
{ ...fixture, payload: { ...payload, id: undefined } },
{ ...fixture, payload: { ...payload, id: '' } },
{ ...fixture, payload: { ...payload, type: undefined } },
{ ...fixture, payload: { ...payload, type: 'composio.trigger.other' } },
{ ...fixture, payload: { ...payload, metadata: undefined } },
{ ...fixture, payload: { ...payload, metadata: [] } },
{ ...fixture, payload: { ...payload, metadata: { ...metadata, trigger_slug: undefined } } },
{ ...fixture, payload: { ...payload, metadata: { ...metadata, trigger_slug: '' } } },
{ ...fixture, payload: { ...payload, metadata: { ...metadata, trigger_id: undefined } } },
{ ...fixture, payload: { ...payload, metadata: { ...metadata, trigger_id: '' } } },
{ ...fixture, payload: { ...payload, metadata: { ...metadata, connected_account_id: undefined } } },
{ ...fixture, payload: { ...payload, metadata: { ...metadata, connected_account_id: '' } } },
{ ...fixture, payload: { ...payload, data: undefined } },
{ ...fixture, payload: { ...payload, data: [] } },
{ ...fixture, payload: { ...payload, data: null } },
{ ...fixture, payload: { ...payload, timestamp: undefined } },
{ ...fixture, payload: { ...payload, timestamp: 'not-a-timestamp' } },
{ ...fixture, payload: { ...payload, timestamp: 42 } },
{ ...fixture, resource: { ...fixture.resource, id: 'different-trigger' } },
{ ...fixture, resource: { ...fixture.resource, path: '/composio/triggers/different-trigger' } },
{ ...fixture, occurredAt: '2026-07-15T09:00:01.000Z' },
{ ...fixture, delivery: undefined },
{ ...fixture, delivery: { ...fixture.delivery, id: undefined } },
{ ...fixture, delivery: { ...fixture.delivery, id: 'different-delivery' } },
{ ...fixture, delivery: { ...fixture.delivery, dedupeKey: undefined } },
{ ...fixture, delivery: { ...fixture.delivery, dedupeKey: 'different-delivery' } }
];
for (const invalid of invalidFrames) {
assert.equal(contract.validate(invalid).valid, false, JSON.stringify(invalid));
assert.equal(safeParseEventFrame(invalid).success, false, JSON.stringify(invalid));
assert.equal(validateSchema(invalid), false, JSON.stringify(invalid));
}

const forwardCompatible = {
...fixture,
payload: {
...payload,
metadata: { ...metadata, future_metadata: { retained: true } },
data: { future_provider_field: true },
future_envelope_field: 'retained'
}
};
assert.deepEqual(contract.validate(forwardCompatible), { valid: true, errors: [] });
assert.equal(safeParseEventFrame(forwardCompatible).success, true);
assert.equal(validateSchema(forwardCompatible), true, JSON.stringify(validateSchema.errors));
});

test('all exported contract schemas register together without identifier collisions', () => {
const ajv = new Ajv2020({ strict: true });
(addFormatsModule as unknown as (instance: Ajv2020) => Ajv2020)(ajv);
addEventContractJsonSchemaKeywords(ajv);
for (const schema of Object.values(EVENT_CONTRACT_JSON_SCHEMAS)) ajv.addSchema(schema);
});

Expand Down
Loading
Loading