@aiwg/cli 2026.8.5 → 2026.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -108,21 +108,30 @@ export async function moveProjectArtifacts(options) {
108
108
  const source = path.resolve(options.from ? expandProjectArtifactPath(options.from, projectDir) : resolveProjectAiwgDir(projectDir));
109
109
  const destination = path.resolve(expandProjectArtifactPath(options.to, projectDir));
110
110
  const dryRun = options.dryRun === true;
111
+ const attach = options.attach === true;
111
112
  const reindex = options.reindex !== false;
112
113
  const syncFortemi = options.syncFortemi !== false;
113
- if (samePath(source, destination)) {
114
+ if (!attach && samePath(source, destination)) {
114
115
  throw new Error(`Source and destination are the same directory: ${source}`);
115
116
  }
116
- if (!existsSync(source)) {
117
+ if (!attach && !existsSync(source)) {
117
118
  throw new Error(`Source AIWG artifact directory does not exist: ${source}`);
118
119
  }
119
- if (existsSync(destination) && !(await isEmptyDirectory(destination))) {
120
+ if (attach) {
121
+ if (!existsSync(destination) || !(await stat(destination)).isDirectory()) {
122
+ throw new Error(`Artifact root to attach does not exist or is not a directory: ${destination}`);
123
+ }
124
+ if (!existsSync(path.join(destination, 'aiwg.config'))) {
125
+ throw new Error(`Artifact root to attach has no aiwg.config: ${destination}`);
126
+ }
127
+ }
128
+ else if (existsSync(destination) && !(await isEmptyDirectory(destination))) {
120
129
  throw new Error(`Destination already exists and is not empty: ${destination}`);
121
130
  }
122
131
  const pointerValue = pointerValueFor(projectDir, destination);
123
132
  const pointerPath = path.join(projectDir, PROJECT_AIWG_LOCATION_FILE);
124
133
  const gitignoreUpdated = await ensureGitignorePointer(projectDir, dryRun);
125
- if (!dryRun) {
134
+ if (!dryRun && !attach) {
126
135
  await moveDirectory(source, destination);
127
136
  }
128
137
  await writePointer(projectDir, pointerValue, dryRun);
@@ -144,7 +153,8 @@ export async function moveProjectArtifacts(options) {
144
153
  to: destination,
145
154
  pointerPath,
146
155
  pointerValue,
147
- moved: !dryRun,
156
+ moved: !dryRun && !attach,
157
+ attached: attach && !dryRun,
148
158
  gitignoreUpdated,
149
159
  reindexed,
150
160
  fortemiSynced,
@@ -5,9 +5,11 @@ function usage() {
5
5
  'aiwg artifacts — Manage the project AIWG artifact root',
6
6
  '',
7
7
  'Usage:',
8
- ' aiwg artifacts move --to <path> [--from <path>] [--dry-run] [--force] [--no-reindex] [--no-sync]',
8
+ ' aiwg artifacts move --to <path> [--from <path>] [--dry-run] [--no-reindex] [--no-sync]',
9
+ ' aiwg artifacts attach --to <existing-path> [--dry-run] [--no-reindex] [--no-sync]',
9
10
  '',
10
11
  'Notes:',
12
+ ' move relocates a local artifact root; attach adopts an existing populated root.',
11
13
  ' --to points at the artifact directory itself, not its parent.',
12
14
  ' AIWG_ARTIFACTS_PATH overrides the generated .aiwg-location pointer.',
13
15
  ].join('\n');
@@ -30,7 +32,7 @@ export const artifactsHandler = {
30
32
  if (action === 'help' || ctx.args.includes('--help') || ctx.args.includes('-h')) {
31
33
  return { exitCode: 0, message: usage() };
32
34
  }
33
- if (action !== 'move') {
35
+ if (action !== 'move' && action !== 'attach') {
34
36
  return { exitCode: 1, message: `Unknown artifacts action: ${action}\n\n${usage()}` };
35
37
  }
36
38
  const to = valueAfter(ctx.args, '--to');
@@ -42,12 +44,15 @@ export const artifactsHandler = {
42
44
  projectDir: getProjectDir(ctx, ctx.args),
43
45
  from: valueAfter(ctx.args, '--from'),
44
46
  to,
47
+ attach: action === 'attach',
45
48
  dryRun: ctx.dryRun || ctx.args.includes('--dry-run'),
46
49
  force: ctx.args.includes('--force'),
47
50
  reindex: !ctx.args.includes('--no-reindex'),
48
51
  syncFortemi: !ctx.args.includes('--no-sync'),
49
52
  });
50
- const verb = result.dryRun ? 'Would move' : 'Moved';
53
+ const verb = result.dryRun
54
+ ? (action === 'attach' ? 'Would attach' : 'Would move')
55
+ : (result.attached ? 'Attached' : 'Moved');
51
56
  return {
52
57
  exitCode: 0,
53
58
  message: [
@@ -415,7 +415,7 @@ CORE TOOLS (15, always registered):
415
415
  command-run Allow-listed CLI dispatch; confirmation-gated when needed
416
416
  artifact-read / artifact-write Project .aiwg/ artifact IO
417
417
 
418
- OPT-IN TOOLSETS (51 additional tools):
418
+ OPT-IN TOOLSETS (60 additional tools):
419
419
  flows flow-list / flow-show / flow-run
420
420
  missions mission-guide / mission-dispatch / mission-status
421
421
  memory memory-* and reflections-* storage operations
@@ -426,9 +426,10 @@ OPT-IN TOOLSETS (51 additional tools):
426
426
  ralph start / status / abort / attach
427
427
  mc start / dispatch / status / stop / list
428
428
  ops status / list / use / push
429
+ sandbox fleet inventory / mutation / reconciliation and governed activity
429
430
 
430
431
  Enable opt-in tools:
431
- AIWG_MCP_TOOLSETS=flows,missions,memory,kb,ralph aiwg mcp serve
432
+ AIWG_MCP_TOOLSETS=flows,missions,memory,kb,ralph,sandbox aiwg mcp serve
432
433
  aiwg mcp serve --toolsets=all
433
434
 
434
435
  RESOURCES:
@@ -451,6 +452,8 @@ TRANSPORTS:
451
452
  ENVIRONMENT:
452
453
  AIWG_ROOT Path to AIWG installation (default: ~/.local/share/ai-writing-guide)
453
454
  AIWG_MCP_TOOLSETS Comma-separated opt-in toolsets; use all for every toolset
455
+ AIWG_SANDBOX_MANAGEMENT_URL Sandbox management API origin (HTTPS except loopback)
456
+ AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE Mode-0600 management bearer file for sandbox tools
454
457
 
455
458
  Docs:
456
459
  docs/integrations/mcp-capability-audit.md
@@ -68,7 +68,7 @@ export function createServer() {
68
68
  // Opt-in subsystem toolsets (#1322-#1332)
69
69
  //
70
70
  // Enabled via AIWG_MCP_TOOLSETS env or `aiwg mcp serve --toolsets=`.
71
- // Known: flows, missions, memory, kb, research, activity-log, index, ralph, mc, ops, all
71
+ // Known: flows, missions, memory, kb, research, activity-log, index, ralph, mc, ops, sandbox, all
72
72
  // Default: none (core only — discovery + command-run)
73
73
  // ============================================
74
74
  const requested = parseToolsets(process.env.AIWG_MCP_TOOLSETS || '');
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Governed Agentic Sandbox fleet and activity MCP surface.
3
+ *
4
+ * Credentials are file-backed server configuration, never tool arguments.
5
+ * Every tool in this module uses the management bearer domain; executor-plane
6
+ * credentials are deliberately out of scope and cannot be substituted.
7
+ *
8
+ * @implements #2015
9
+ */
10
+
11
+ import { readFile, stat } from 'node:fs/promises';
12
+ import { z } from 'zod';
13
+ import { mcpError, mcpJson } from '../helpers.mjs';
14
+
15
+ const FLEET_VERSION = 'agentic-orchestration/v1';
16
+ const ACTIVITY_VERSION = 'activity.event/v1';
17
+ const SCOPE_HEADERS = {
18
+ tenant_id: 'x-agentic-tenant-id',
19
+ host_id: 'x-agentic-host-id',
20
+ instance_id: 'x-agentic-instance-id',
21
+ agent_id: 'x-agentic-agent-id',
22
+ };
23
+ const RESTRICTED_KEY = /(?:^|_)(?:content|terminal|prompt|environment|env|credential|secret|password|authorization|bearer|token|private_key|certificate|restricted_(?:url|uri|link))(?:$|_)/i;
24
+
25
+ function configuredBaseUrl(env = process.env) {
26
+ const raw = String(env.AIWG_SANDBOX_MANAGEMENT_URL ?? '').trim();
27
+ if (!raw) throw new Error('AIWG_SANDBOX_MANAGEMENT_URL is required for the sandbox toolset');
28
+ const url = new URL(raw);
29
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
30
+ throw new Error('AIWG_SANDBOX_MANAGEMENT_URL must be an HTTP(S) origin without credentials, query, or fragment');
31
+ }
32
+ if (url.protocol !== 'https:' && !['127.0.0.1', 'localhost', '::1'].includes(url.hostname)) {
33
+ throw new Error('AIWG_SANDBOX_MANAGEMENT_URL requires HTTPS outside loopback');
34
+ }
35
+ return url.toString().replace(/\/$/, '');
36
+ }
37
+
38
+ async function managementBearer(env = process.env) {
39
+ const tokenFile = String(env.AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE ?? '').trim();
40
+ if (!tokenFile) throw new Error('AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE is required for the sandbox toolset');
41
+ const metadata = await stat(tokenFile);
42
+ if (!metadata.isFile()) throw new Error('sandbox management token path is not a regular file');
43
+ if (process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) {
44
+ throw new Error('sandbox management token file must not be accessible by group or other users');
45
+ }
46
+ const token = String(await readFile(tokenFile, 'utf8')).trim();
47
+ if (!token || /[\r\n]/.test(token)) throw new Error('sandbox management token file must contain one non-empty bearer token');
48
+ return token;
49
+ }
50
+
51
+ function containsRestricted(value) {
52
+ if (Array.isArray(value)) return value.some(containsRestricted);
53
+ if (!value || typeof value !== 'object') return false;
54
+ return Object.entries(value).some(([key, child]) => RESTRICTED_KEY.test(key) || containsRestricted(child));
55
+ }
56
+
57
+ function requireSafePayload(value, label) {
58
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object`);
59
+ if (containsRestricted(value)) throw new Error(`${label} contains credential or restricted-content fields`);
60
+ return value;
61
+ }
62
+
63
+ function safeResponse(value) {
64
+ if (containsRestricted(value)) throw Object.assign(new Error('sandbox response contained prohibited credential or restricted-content fields'), { code: 'sandbox_restricted_response' });
65
+ return value;
66
+ }
67
+
68
+ function requireFleetRecord(record) {
69
+ requireSafePayload(record, 'fleet workload');
70
+ if (record.document_type !== 'workload' || record.api_version !== FLEET_VERSION) throw new Error(`fleet workload must use ${FLEET_VERSION}`);
71
+ if (!record.lineage || typeof record.lineage.child_id !== 'string' || !record.lineage.child_id) throw new Error('fleet workload requires lineage.child_id');
72
+ if (!record.status || !Number.isInteger(record.status.revision) || record.status.revision < 0) throw new Error('fleet workload requires a non-negative status.revision');
73
+ return record;
74
+ }
75
+
76
+ function requireInventory(value) {
77
+ safeResponse(value);
78
+ if (value?.document_type !== 'inventory' || value?.api_version !== FLEET_VERSION || !Number.isInteger(value?.inventory_revision) || !Array.isArray(value?.records)) {
79
+ throw new Error('invalid fleet inventory envelope');
80
+ }
81
+ value.records.forEach(requireFleetRecord);
82
+ return value;
83
+ }
84
+
85
+ function requireReconciliation(value) {
86
+ safeResponse(value);
87
+ if (value?.document_type !== 'reconciliation' || value?.api_version !== FLEET_VERSION || !Number.isInteger(value?.before_revision) || !Number.isInteger(value?.after_revision) || !Array.isArray(value?.rows)) {
88
+ throw new Error('invalid fleet reconciliation envelope');
89
+ }
90
+ return value;
91
+ }
92
+
93
+ function requireActivityEnvelope(value, scope, { eventsRequired = false, exportEnvelope = false } = {}) {
94
+ safeResponse(value);
95
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid activity envelope');
96
+ if (!exportEnvelope && value.schema_version !== ACTIVITY_VERSION) throw new Error(`activity envelope must use ${ACTIVITY_VERSION}`);
97
+ if (!exportEnvelope && (!Array.isArray(value.coverage) || typeof value.completeness?.complete !== 'boolean')) throw new Error('activity envelope requires coverage and completeness');
98
+ if (eventsRequired && !Array.isArray(value.events)) throw new Error('activity timeline requires events');
99
+ for (const event of value.events ?? []) {
100
+ if (
101
+ event?.schema_version !== ACTIVITY_VERSION
102
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(event?.event_id ?? '')
103
+ || !/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(event?.event_name ?? '')
104
+ || !['session', 'action', 'network', 'runtime', 'system', 'integrity'].includes(event?.plane)
105
+ || Number.isNaN(Date.parse(event?.occurred_at ?? ''))
106
+ || Number.isNaN(Date.parse(event?.observed_at ?? ''))
107
+ || !event?.source || typeof event.source.collector !== 'string' || !event.source.collector
108
+ || !['guest', 'runtime', 'host', 'control-plane', 'provider'].includes(event.source.layer)
109
+ || !['qemu-kvm', 'cloud-hypervisor', 'docker', 'host', 'unknown'].includes(event.source.runtime)
110
+ || !['observed', 'attested', 'self-reported', 'derived'].includes(event.source.trust)
111
+ || event?.sensitivity !== 'metadata'
112
+ || !['standard', 'security', 'forensic-hold', 'ephemeral'].includes(event?.retention_class)
113
+ || !event?.payload || typeof event.payload !== 'object' || Array.isArray(event.payload)
114
+ || !Number.isInteger(event?.integrity?.collector_sequence) || event.integrity.collector_sequence < 1
115
+ ) throw new Error('activity event violates schema or sensitivity policy');
116
+ for (const [key, expected] of Object.entries(scope)) if (event?.correlation?.[key] !== expected) throw new Error(`activity event ${key} scope mismatch`);
117
+ }
118
+ if (exportEnvelope) {
119
+ const manifest = value.manifest;
120
+ if (!manifest || manifest.tenant_id !== scope.tenant_id || !Number.isInteger(manifest.event_count) || manifest.event_count < 0 || !/^[0-9a-f]{64}$/.test(manifest.merkle_root ?? '') || typeof manifest.key_id !== 'string' || !manifest.key_id || typeof manifest.signature !== 'string' || !manifest.signature) {
121
+ throw new Error('activity export manifest is malformed or out of scope');
122
+ }
123
+ }
124
+ return value;
125
+ }
126
+
127
+ function typedResult(body, status, validate) {
128
+ if (status === 404 || status === 405) return mcpJson({ supported: false, reason: 'capability_absent', status });
129
+ if (status < 200 || status >= 300) {
130
+ const safe = safeResponse(body);
131
+ return {
132
+ ...mcpJson({ supported: true, ok: false, status, error_code: safe?.error ?? safe?.code ?? `http_${status}`, details: safe }),
133
+ isError: true,
134
+ };
135
+ }
136
+ return mcpJson({ supported: true, ok: true, status, data: validate(body) });
137
+ }
138
+
139
+ export class AgenticSandboxMcpClient {
140
+ constructor({ env = process.env, fetch = globalThis.fetch.bind(globalThis) } = {}) {
141
+ this.env = env;
142
+ this.fetch = fetch;
143
+ }
144
+
145
+ async request(path, { method = 'GET', body, headers = {}, validate = (value) => safeResponse(value) } = {}) {
146
+ const baseUrl = configuredBaseUrl(this.env);
147
+ const token = await managementBearer(this.env);
148
+ const response = await this.fetch(`${baseUrl}${path}`, {
149
+ method,
150
+ headers: { accept: 'application/json', authorization: `Bearer ${token}`, ...(body === undefined ? {} : { 'content-type': 'application/json' }), ...headers },
151
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
152
+ });
153
+ let parsed;
154
+ try {
155
+ parsed = await response.json();
156
+ } catch {
157
+ if (response.status === 404 || response.status === 405) parsed = {};
158
+ else return mcpError(`sandbox returned non-JSON HTTP ${response.status}`);
159
+ }
160
+ try {
161
+ return typedResult(parsed, response.status, validate);
162
+ } catch (error) {
163
+ return mcpError(`${error.code ?? 'sandbox_malformed_response'}: ${error.message}`);
164
+ }
165
+ }
166
+ }
167
+
168
+ const scopeSchema = {
169
+ tenant_id: z.string().min(1).max(255).regex(/^[^\r\n]+$/),
170
+ host_id: z.string().min(1).max(255).regex(/^[^\r\n]+$/),
171
+ instance_id: z.string().min(1).max(255).regex(/^[^\r\n]+$/),
172
+ agent_id: z.string().min(1).max(255).regex(/^[^\r\n]+$/),
173
+ };
174
+ const activityFilterSchema = z.object({
175
+ event_name: z.string().min(1).max(255).optional(), collector: z.string().min(1).max(255).optional(),
176
+ trust: z.string().min(1).max(255).optional(), plane: z.string().min(1).max(255).optional(),
177
+ outcome: z.string().min(1).max(255).optional(), session_id: z.string().min(1).max(255).optional(),
178
+ mission_id: z.string().min(1).max(255).optional(), task_id: z.string().min(1).max(255).optional(),
179
+ tool_call_id: z.string().min(1).max(255).optional(), command_id: z.string().min(1).max(255).optional(),
180
+ process_id: z.string().min(1).max(255).optional(), trace_id: z.string().regex(/^[0-9a-f]{32}$/).optional(),
181
+ since: z.string().datetime().optional(), until: z.string().datetime().optional(), limit: z.number().int().min(1).max(1000).optional(),
182
+ }).strict().optional();
183
+
184
+ function scopeHeaders(args) {
185
+ return Object.fromEntries(Object.entries(SCOPE_HEADERS).map(([key, header]) => [header, args[key]]));
186
+ }
187
+
188
+ function activityQuery(filter = {}) {
189
+ const query = new URLSearchParams();
190
+ for (const [key, value] of Object.entries(filter)) query.set(key, String(value));
191
+ return query.size ? `?${query}` : '';
192
+ }
193
+
194
+ function confirmationError(name) {
195
+ return mcpError(`${name} requires confirmed=true`, { requiresConfirmation: true, remediation: 'Review the exact scope and payload, then re-invoke with confirmed=true.' });
196
+ }
197
+
198
+ export function registerAgenticSandboxToolset(server, { client = new AgenticSandboxMcpClient() } = {}) {
199
+ const register = (name, config, handler) => server.registerTool(name, config, async (args) => {
200
+ try { return await handler(args); } catch (error) { return mcpError(`${name}: ${error.message}`); }
201
+ });
202
+
203
+ register('sandbox-fleet-list', { title: 'List Agentic Sandbox fleet workloads', description: 'Read revisioned v2026.8.3+ fleet inventory using the management credential domain.', inputSchema: { contract_version: z.literal(FLEET_VERSION).default(FLEET_VERSION) }, annotations: { readOnlyHint: true, destructiveHint: false } },
204
+ () => client.request('/api/v2/fleet/workloads', { validate: requireInventory }));
205
+ register('sandbox-fleet-get', { title: 'Get Agentic Sandbox fleet workload', description: 'Read one revisioned fleet workload by child identity.', inputSchema: { contract_version: z.literal(FLEET_VERSION).default(FLEET_VERSION), child_id: z.string().min(1).max(255) }, annotations: { readOnlyHint: true, destructiveHint: false } },
206
+ ({ child_id }) => client.request(`/api/v2/fleet/workloads/${encodeURIComponent(child_id)}`, { validate: requireFleetRecord }));
207
+ register('sandbox-fleet-reconcile-preview', { title: 'Preview fleet reconciliation', description: 'Compute a read-only reconciliation preview from inventory; never calls POST /reconcile.', inputSchema: { contract_version: z.literal(FLEET_VERSION).default(FLEET_VERSION), before_revision: z.number().int().min(0), child_ids: z.array(z.string().min(1).max(255)).max(1000) }, annotations: { readOnlyHint: true, destructiveHint: false } },
208
+ async ({ before_revision, child_ids }) => client.request('/api/v2/fleet/workloads', { validate: (value) => {
209
+ const inventory = requireInventory(value);
210
+ const byId = new Map(inventory.records.map((record) => [record.lineage.child_id, record]));
211
+ return { document_type: 'reconciliation-preview', api_version: FLEET_VERSION, before_revision, inventory_revision: inventory.inventory_revision, stale: before_revision !== inventory.inventory_revision, rows: child_ids.map((child_id) => ({ child_id, present: byId.has(child_id), observed_state: byId.get(child_id)?.status?.observed_state ?? 'unknown', revision: byId.get(child_id)?.status?.revision ?? null })) };
212
+ } }));
213
+ register('sandbox-fleet-admit', { title: 'Admit Agentic Sandbox fleet workload', description: 'Mutating fleet admission; requires an exact v1 workload and confirmed=true.', inputSchema: { contract_version: z.literal(FLEET_VERSION).default(FLEET_VERSION), workload: z.record(z.unknown()), confirmed: z.boolean().default(false) }, annotations: { readOnlyHint: false, destructiveHint: true } },
214
+ ({ workload, confirmed }) => confirmed ? client.request('/api/v2/fleet/workloads', { method: 'POST', body: requireFleetRecord(workload), validate: (value) => ({ replayed: value?.replayed === true, workload: requireFleetRecord(value?.workload) }) }) : confirmationError('sandbox-fleet-admit'));
215
+ register('sandbox-fleet-observe', { title: 'Record fleet workload observation', description: 'Mutating monotonic observation update; requires confirmed=true and expected revision.', inputSchema: { contract_version: z.literal(FLEET_VERSION).default(FLEET_VERSION), child_id: z.string().min(1).max(255), expected_revision: z.number().int().min(0), status: z.record(z.unknown()), runtime_identity: z.object({ session_id: z.string().optional(), task_id: z.string().optional(), command_id: z.string().optional() }).strict().optional(), confirmed: z.boolean().default(false) }, annotations: { readOnlyHint: false, destructiveHint: true } },
216
+ ({ child_id, expected_revision, status, runtime_identity, confirmed }) => confirmed ? client.request(`/api/v2/fleet/workloads/${encodeURIComponent(child_id)}/observations`, { method: 'POST', body: requireSafePayload({ expected_revision, status, ...(runtime_identity ? { runtime_identity } : {}) }, 'fleet observation'), validate: requireFleetRecord }) : confirmationError('sandbox-fleet-observe'));
217
+ register('sandbox-fleet-reconcile', { title: 'Reconcile fleet workloads', description: 'Mutating restart reconciliation; requires confirmed=true.', inputSchema: { contract_version: z.literal(FLEET_VERSION).default(FLEET_VERSION), before_revision: z.number().int().min(0), child_ids: z.array(z.string().min(1).max(255)).max(1000), confirmed: z.boolean().default(false) }, annotations: { readOnlyHint: false, destructiveHint: true } },
218
+ ({ before_revision, child_ids, confirmed }) => confirmed ? client.request('/api/v2/fleet/reconcile', { method: 'POST', body: { before_revision, child_ids }, validate: requireReconciliation }) : confirmationError('sandbox-fleet-reconcile'));
219
+
220
+ for (const [kind, eventsRequired] of [['coverage', false], ['timeline', true]]) {
221
+ register(`sandbox-activity-${kind}`, { title: `${kind === 'coverage' ? 'Inspect coverage for' : 'Read timeline from'} Agentic Sandbox activity`, description: `Read governed, exactly scoped activity ${kind}; preserves capability and authorization status.`, inputSchema: { contract_version: z.literal(ACTIVITY_VERSION).default(ACTIVITY_VERSION), ...scopeSchema, filter: activityFilterSchema }, annotations: { readOnlyHint: true, destructiveHint: false } },
222
+ (args) => { const scope = Object.fromEntries(Object.keys(SCOPE_HEADERS).map((key) => [key, args[key]])); return client.request(`/api/v2/activity/${kind}${activityQuery(args.filter)}`, { headers: scopeHeaders(args), validate: (value) => requireActivityEnvelope(value, scope, { eventsRequired }) }); });
223
+ }
224
+ register('sandbox-activity-export', { title: 'Export signed Agentic Sandbox activity evidence', description: 'Evidence export requires confirmed=true even though it does not mutate server state.', inputSchema: { contract_version: z.literal(ACTIVITY_VERSION).default(ACTIVITY_VERSION), ...scopeSchema, filter: activityFilterSchema, confirmed: z.boolean().default(false) }, annotations: { readOnlyHint: false, destructiveHint: true } },
225
+ (args) => { if (!args.confirmed) return confirmationError('sandbox-activity-export'); const scope = Object.fromEntries(Object.keys(SCOPE_HEADERS).map((key) => [key, args[key]])); return client.request('/api/v2/activity/export', { method: 'POST', headers: scopeHeaders(args), body: args.filter ?? {}, validate: (value) => requireActivityEnvelope(value, scope, { eventsRequired: true, exportEnvelope: true }) }); });
226
+ }
227
+
228
+ export const AGENTIC_SANDBOX_TOOL_NAMES = [
229
+ 'sandbox-fleet-list', 'sandbox-fleet-get', 'sandbox-fleet-reconcile-preview',
230
+ 'sandbox-fleet-admit', 'sandbox-fleet-observe', 'sandbox-fleet-reconcile',
231
+ 'sandbox-activity-coverage', 'sandbox-activity-timeline', 'sandbox-activity-export',
232
+ ];
@@ -17,6 +17,7 @@
17
17
  import { z } from 'zod';
18
18
  import { runAiwgCli, mcpError, mcpJson } from '../helpers.mjs';
19
19
  import { registerFlowToolset, registerMissionToolset } from './orchestration.mjs';
20
+ import { registerAgenticSandboxToolset } from './agentic-sandbox.mjs';
20
21
 
21
22
  /**
22
23
  * Wrap an `aiwg <subsystem> <verb>` CLI call as an MCP tool.
@@ -586,6 +587,7 @@ const TOOLSET_REGISTRY = {
586
587
  ralph: registerRalphToolset,
587
588
  mc: registerMcToolset,
588
589
  ops: registerOpsToolset,
590
+ sandbox: registerAgenticSandboxToolset,
589
591
  };
590
592
 
591
593
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.5",
3
+ "version": "2026.8.7",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",