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
18 changes: 16 additions & 2 deletions packages/command-registry/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1209,7 +1209,14 @@ export const RAW_COMMAND_DESCRIPTORS = [
{
name: 'record',
deviceClaimPolicy: 'require-owner',
...(ownerFilesEnabled ? { ownerFiles: ['src/commands/recording/index.ts'] as const } : {}),
...(ownerFilesEnabled
? {
ownerFiles: [
'src/commands/recording/index.ts',
'src/commands/recording/output-schemas.ts',
] as const,
}
: {}),
catalog: { group: 'public' },
frameworkTier: 'extended',
recordsSessionAction: true,
Expand All @@ -1227,7 +1234,14 @@ export const RAW_COMMAND_DESCRIPTORS = [
{
name: 'trace',
deviceClaimPolicy: 'require-owner',
...(ownerFilesEnabled ? { ownerFiles: ['src/commands/recording/index.ts'] as const } : {}),
...(ownerFilesEnabled
? {
ownerFiles: [
'src/commands/recording/index.ts',
'src/commands/recording/output-schemas.ts',
] as const,
}
: {}),
catalog: { group: 'public' },
frameworkTier: 'extended',
recordsSessionAction: true,
Expand Down
97 changes: 97 additions & 0 deletions src/commands/recording/output-schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { NATIVE_PATH_DISPOSITION_VALUES } from '@agent-device/contracts/recording-native-path';
import { RECORDER_OBSERVATION_VALUES } from '@agent-device/contracts/recording-stop-observation';
import type { CommandResultMap } from '@agent-device/command-registry/command-result';
import type { JsonSchema } from '../command-contract.ts';
import {
booleanSchema,
constSchema,
enumSchema,
looseObjectSchema,
numberSchema,
objectSchema,
stringSchema,
} from '../command-input.ts';

const artifactSchema = objectSchema(
{
field: stringSchema(),
artifactType: stringSchema(),
path: stringSchema(),
localPath: stringSchema(),
fileName: stringSchema(),
},
['field'],
);

/**
* This family's advertised MCP `outputSchema`s, keyed by daemon command name and projected into
* the command map by `src/mcp/command-output-schemas.ts`. Non-strict like every other entry: no
* `additionalProperties: false`, so additive response fields keep validating. Neither command
* carries the post-action observation trait (#1652): both fire-and-report a recording or trace
* lifecycle change, not an interaction, so no settle-graft copy applies here.
*/
export const RECORDING_COMMAND_OUTPUT_SCHEMAS = {
// packages/contracts/src/recording.ts
record: {
type: 'object',
oneOf: [
objectSchema(
{
recording: constSchema('started'),
outPath: stringSchema(),
sessionStateDir: stringSchema(),
recordingBackend: stringSchema(),
recordingScope: stringSchema(),
recordOnlySession: booleanSchema(),
activeSessionApp: looseObjectSchema(),
showTouches: booleanSchema(),
},
['recording', 'outPath', 'sessionStateDir', 'showTouches'],
),
objectSchema(
{
recording: constSchema('stopped'),
outPath: stringSchema(),
telemetryPath: stringSchema(),
artifacts: { type: 'array', items: artifactSchema },
recordingBackend: stringSchema(),
recordingScope: stringSchema(),
recordOnlySession: booleanSchema(),
activeSessionApp: looseObjectSchema(),
durationMs: numberSchema(),
capturedDurationMs: numberSchema(),
recorder: enumSchema(
RECORDER_OBSERVATION_VALUES,
'What the recorder was observed doing when the recording was stopped: confirmed, or lost when the session holding it died. ADR 0024 reserves unconfirmed for the step that gains the probe.',
),
nativePathDisposition: enumSchema(
NATIVE_PATH_DISPOSITION_VALUES,
'What became of the artifact path the recorder writes to: retirable while it still owes a removal, retired once that removal was verified. ADR 0024 reserves pending.',
),
showTouches: booleanSchema(),
warning: stringSchema(),
overlayWarning: stringSchema(),
chunks: { type: 'array', items: looseObjectSchema() },
},
['recording', 'outPath', 'artifacts', 'durationMs', 'showTouches'],
),
],
},
trace: {
type: 'object',
oneOf: [
objectSchema({ trace: constSchema('started'), outPath: stringSchema() }, [
'trace',
'outPath',
]),
objectSchema(
{
trace: constSchema('stopped'),
outPath: stringSchema(),
artifacts: { type: 'array', items: artifactSchema },
},
['trace', 'outPath', 'artifacts'],
),
],
},
} satisfies Pick<Record<keyof CommandResultMap, JsonSchema>, 'record' | 'trace'>;
28 changes: 28 additions & 0 deletions src/mcp/__tests__/command-tools-recording-schemas.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files';
import { RECORDING_COMMAND_OUTPUT_SCHEMAS } from '../../commands/recording/output-schemas.ts';
import { COMMAND_OUTPUT_SCHEMAS } from '../command-output-schemas.ts';
import { validateAgainstSchema } from './output-schema-validator.ts';

const RECORDING_COMMANDS = Object.keys(RECORDING_COMMAND_OUTPUT_SCHEMAS) as Array<
keyof typeof RECORDING_COMMAND_OUTPUT_SCHEMAS
>;

// Neither record nor trace carries the post-action observation trait (#1652): both fire-and-report
// a recording or trace lifecycle change, not an interaction, so the composed map never copies
// either entry and reference equality must hold for both.
test('MCP recording family output schemas are the family module entries, not copies', () => {
for (const command of RECORDING_COMMANDS) {
assert.equal(
COMMAND_OUTPUT_SCHEMAS[command],
RECORDING_COMMAND_OUTPUT_SCHEMAS[command],
`${command} is not reference-equal to the recording module's own schema object`,
);
}
});

test('the projected family map declares exactly the commands its module owns', () => {
for (const command of RECORDING_COMMANDS) {
assert.ok(
ownerFilesForCommand(command).includes('src/commands/recording/output-schemas.ts'),
`${command} projects its output schema from this module but does not name it as its owner`,
);
}
});

// `record stop` answers two independent questions (ADR 0024): whether a playable export exists, and
// whether its recorder stopped. Only the second one's words are additive properties here — the
// stopped branch's required list is what it was before either fact was reported.
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/__tests__/command-tools-replay-schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { commandSupportsSettleObservation } from '@agent-device/command-registry
import { DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/device.ts';
import { PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/push.ts';
import { INTERACTION_COMMAND_OUTPUT_SCHEMAS } from '../../commands/interaction/index.ts';
import { RECORDING_COMMAND_OUTPUT_SCHEMAS } from '../../commands/recording/output-schemas.ts';
import { REPLAY_COMMAND_OUTPUT_SCHEMAS } from '../../commands/replay/index.ts';
import { SYSTEM_COMMAND_OUTPUT_SCHEMAS } from '../../commands/system/index.ts';
import { COMMAND_OUTPUT_SCHEMAS } from '../command-output-schemas.ts';
Expand Down Expand Up @@ -77,6 +78,7 @@ const PROJECTED_FAMILIES = [
{ name: 'interaction', schemas: INTERACTION_COMMAND_OUTPUT_SCHEMAS },
{ name: 'management-device', schemas: DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS },
{ name: 'management-push', schemas: PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS },
{ name: 'recording', schemas: RECORDING_COMMAND_OUTPUT_SCHEMAS },
] as const;

test('projected output-schema families claim disjoint commands and survive the composition intact', () => {
Expand Down
79 changes: 2 additions & 77 deletions src/mcp/command-output-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ import {
INTERACTION_COMMAND_OUTPUT_SCHEMAS,
postActionSurfaceChangeSchema,
} from '../commands/interaction/index.ts';
import { RECORDING_COMMAND_OUTPUT_SCHEMAS } from '../commands/recording/output-schemas.ts';
import { REPLAY_COMMAND_OUTPUT_SCHEMAS } from '../commands/replay/index.ts';
import { SYSTEM_COMMAND_OUTPUT_SCHEMAS } from '../commands/system/index.ts';
import { NATIVE_PATH_DISPOSITION_VALUES } from '@agent-device/contracts/recording-native-path';
import { RECORDER_OBSERVATION_VALUES } from '@agent-device/contracts/recording-stop-observation';
import { DEVICE_KINDS, DEVICE_TARGETS, PUBLIC_PLATFORMS } from '@agent-device/kernel/device';

/**
Expand Down Expand Up @@ -52,17 +51,6 @@ import { DEVICE_KINDS, DEVICE_TARGETS, PUBLIC_PLATFORMS } from '@agent-device/ke
* trait (#1652).
*/

const artifactSchema = objectSchema(
{
field: stringSchema(),
artifactType: stringSchema(),
path: stringSchema(),
localPath: stringSchema(),
fileName: stringSchema(),
},
['field'],
);

// SettleObservation (packages/contracts/src/interaction.ts) — opt-in `--settle` settled
// diff observation (#1101).
const settleObservationSchema: JsonSchema = objectSchema(
Expand Down Expand Up @@ -330,77 +318,14 @@ const BASE_COMMAND_OUTPUT_SCHEMAS = {
['mode', 'baselineInitialized', 'summary', 'lines'],
),

// packages/contracts/src/recording.ts
record: {
type: 'object',
oneOf: [
objectSchema(
{
recording: constSchema('started'),
outPath: stringSchema(),
sessionStateDir: stringSchema(),
recordingBackend: stringSchema(),
recordingScope: stringSchema(),
recordOnlySession: booleanSchema(),
activeSessionApp: looseObjectSchema(),
showTouches: booleanSchema(),
},
['recording', 'outPath', 'sessionStateDir', 'showTouches'],
),
objectSchema(
{
recording: constSchema('stopped'),
outPath: stringSchema(),
telemetryPath: stringSchema(),
artifacts: { type: 'array', items: artifactSchema },
recordingBackend: stringSchema(),
recordingScope: stringSchema(),
recordOnlySession: booleanSchema(),
activeSessionApp: looseObjectSchema(),
durationMs: numberSchema(),
capturedDurationMs: numberSchema(),
recorder: enumSchema(
RECORDER_OBSERVATION_VALUES,
'What the recorder was observed doing when the recording was stopped: confirmed, or lost when the session holding it died. ADR 0024 reserves unconfirmed for the step that gains the probe.',
),
nativePathDisposition: enumSchema(
NATIVE_PATH_DISPOSITION_VALUES,
'What became of the artifact path the recorder writes to: retirable while it still owes a removal, retired once that removal was verified. ADR 0024 reserves pending.',
),
showTouches: booleanSchema(),
warning: stringSchema(),
overlayWarning: stringSchema(),
chunks: { type: 'array', items: looseObjectSchema() },
},
['recording', 'outPath', 'artifacts', 'durationMs', 'showTouches'],
),
],
},
trace: {
type: 'object',
oneOf: [
objectSchema({ trace: constSchema('started'), outPath: stringSchema() }, [
'trace',
'outPath',
]),
objectSchema(
{
trace: constSchema('stopped'),
outPath: stringSchema(),
artifacts: { type: 'array', items: artifactSchema },
},
['trace', 'outPath', 'artifacts'],
),
],
},

// A family that owns its commands authors their advertised response shape beside the
// command surface and projects it here. This spread stays last: a hand-written entry for
// a projected command then fails as TS2783 instead of quietly overriding the family's,
// and this map's `satisfies` still refuses a missing `CommandResultMap` key.
...DEVICE_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS,
...PUSH_MANAGEMENT_COMMAND_OUTPUT_SCHEMAS,
...INTERACTION_COMMAND_OUTPUT_SCHEMAS,
...RECORDING_COMMAND_OUTPUT_SCHEMAS,
...REPLAY_COMMAND_OUTPUT_SCHEMAS,
...SYSTEM_COMMAND_OUTPUT_SCHEMAS,
} satisfies Record<keyof CommandResultMap, JsonSchema>;
Expand Down
Loading