@sidurijs/hands 1.0.0

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.
@@ -0,0 +1,407 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("./index");
4
+ const core_1 = require("@sidurijs/core");
5
+ describe('DefaultHandsOrgan Adversarial Remediation Suite', () => {
6
+ const secretKey = 'test_hands_secret';
7
+ let engine;
8
+ let sampleContext;
9
+ beforeEach(() => {
10
+ engine = new core_1.ActionPolicyEngine({
11
+ secretKey,
12
+ defaultRiskLevel: 'LOW',
13
+ });
14
+ sampleContext = {
15
+ companionId: 'comp-1',
16
+ actor: {
17
+ actorId: 'user-1',
18
+ sessionId: 'sess-1',
19
+ authorizationRole: 'operator',
20
+ capabilities: ['tool:calc', 'tool:search', 'tool:complex'],
21
+ authenticated: true,
22
+ },
23
+ conversation: {
24
+ channel: 'direct',
25
+ correlationId: 'corr-1',
26
+ },
27
+ };
28
+ });
29
+ describe('P0 — Elimination of Hands Authorization Bypass', () => {
30
+ it('rejects execution when authorization is missing or undefined', async () => {
31
+ let executed = false;
32
+ const hands = new index_1.DefaultHandsOrgan({ secretKey });
33
+ hands.registerTool({
34
+ definition: { name: 'calc', inputSchema: {}, description: 'calc' },
35
+ execute: async () => { executed = true; return 42; },
36
+ });
37
+ const res = await hands.executeAction({ actionId: 'act-1', toolName: 'calc', parameters: {} }, undefined);
38
+ expect(res.success).toBe(false);
39
+ expect(res.lifecycle).toBe('REJECTED');
40
+ expect(res.error).toContain('Missing mandatory AuthorizationCapability');
41
+ expect(executed).toBe(false);
42
+ });
43
+ it('rejects forged { allowed: true } without valid policy signature', async () => {
44
+ let executed = false;
45
+ const hands = new index_1.DefaultHandsOrgan({ secretKey });
46
+ hands.registerTool({
47
+ definition: { name: 'admin_tool', inputSchema: {}, description: 'admin' },
48
+ execute: async () => { executed = true; return 'pwned'; },
49
+ });
50
+ const forgedCapability = {
51
+ executionId: 'exec-forged',
52
+ actionId: 'act-forged',
53
+ toolName: 'admin_tool',
54
+ providerId: 'builtin',
55
+ parametersHash: 'h_forged',
56
+ companionId: 'comp-1',
57
+ riskLevel: 'LOW',
58
+ issuedAt: new Date().toISOString(),
59
+ expiresAt: new Date(Date.now() + 60000).toISOString(),
60
+ allowed: true,
61
+ signature: 'fake_forged_sig',
62
+ };
63
+ const res = await hands.executeAction({ actionId: 'act-forged', toolName: 'admin_tool', parameters: {} }, forgedCapability);
64
+ expect(res.success).toBe(false);
65
+ expect(res.lifecycle).toBe('REJECTED');
66
+ expect(res.error).toContain('Invalid or forged AuthorizationCapability signature');
67
+ expect(executed).toBe(false);
68
+ });
69
+ it('rejects authorization with mismatched actionId, toolName, or parameter hash', async () => {
70
+ const hands = new index_1.DefaultHandsOrgan({ secretKey });
71
+ hands.registerTool({
72
+ definition: { name: 'search', inputSchema: {}, description: 'search' },
73
+ execute: async () => ({ results: [] }),
74
+ });
75
+ engine.registerToolDefinition({ name: 'search', inputSchema: {}, description: 'search', riskLevel: 'LOW' });
76
+ // Generate authentic capability for parameters { q: 'foo' }
77
+ const { capability } = await engine.evaluateAction({
78
+ actionId: 'act-auth-1',
79
+ toolName: 'search',
80
+ parameters: { q: 'foo' },
81
+ context: sampleContext,
82
+ });
83
+ expect(capability).toBeDefined();
84
+ // Attack: Mismatched parameters (tampered from 'foo' to 'malicious')
85
+ const tamperedRes = await hands.executeAction({ actionId: 'act-auth-1', toolName: 'search', parameters: { q: 'malicious' } }, capability);
86
+ expect(tamperedRes.success).toBe(false);
87
+ expect(tamperedRes.lifecycle).toBe('REJECTED');
88
+ expect(tamperedRes.error).toContain('Parameters hash mismatch');
89
+ // Attack: Mismatched actionId
90
+ const mismatchedActionRes = await hands.executeAction({ actionId: 'act-different-id', toolName: 'search', parameters: { q: 'foo' } }, capability);
91
+ expect(mismatchedActionRes.success).toBe(false);
92
+ expect(mismatchedActionRes.error).toContain('ActionId mismatch');
93
+ });
94
+ it('executes when authentic policy-issued AuthorizationCapability is presented', async () => {
95
+ let executed = false;
96
+ const hands = new index_1.DefaultHandsOrgan({ secretKey });
97
+ hands.registerTool({
98
+ definition: { name: 'search', inputSchema: {}, description: 'search' },
99
+ execute: async (p) => { executed = true; return { ok: true, query: p.q }; },
100
+ });
101
+ engine.registerToolDefinition({ name: 'search', inputSchema: {}, description: 'search', riskLevel: 'LOW' });
102
+ const action = {
103
+ actionId: 'act-legit-1',
104
+ toolName: 'search',
105
+ parameters: { q: 'hello' },
106
+ context: sampleContext,
107
+ };
108
+ const { capability } = await engine.evaluateAction(action);
109
+ expect(capability).toBeDefined();
110
+ const res = await hands.executeAction(action, capability);
111
+ expect(res.success).toBe(true);
112
+ expect(res.lifecycle).toBe('COMPLETED');
113
+ expect(executed).toBe(true);
114
+ expect(res.result.query).toBe('hello');
115
+ });
116
+ it('rejects tampered companionId, executionId, expiresAt, and cross-companion capability reuse', async () => {
117
+ const hands = new index_1.DefaultHandsOrgan({ secretKey });
118
+ hands.registerTool({
119
+ definition: { name: 'search', inputSchema: {}, description: 'search' },
120
+ execute: async () => ({ results: [] }),
121
+ });
122
+ engine.registerToolDefinition({ name: 'search', inputSchema: {}, description: 'search', riskLevel: 'LOW' });
123
+ const action = {
124
+ actionId: 'act-tamper-1',
125
+ toolName: 'search',
126
+ parameters: { q: 'secure' },
127
+ context: sampleContext,
128
+ };
129
+ const { capability } = await engine.evaluateAction(action);
130
+ expect(capability).toBeDefined();
131
+ // Tamper companionId
132
+ const tamperedCompanionCap = { ...capability, companionId: 'attacker-companion' };
133
+ const compRes = await hands.executeAction(action, tamperedCompanionCap);
134
+ expect(compRes.success).toBe(false);
135
+ expect(compRes.error).toContain('Invalid or forged AuthorizationCapability signature');
136
+ // Tamper executionId
137
+ const tamperedExecCap = { ...capability, executionId: 'exec-hijacked' };
138
+ const execRes = await hands.executeAction(action, tamperedExecCap);
139
+ expect(execRes.success).toBe(false);
140
+ expect(execRes.error).toContain('Invalid or forged AuthorizationCapability signature');
141
+ // Expired capability
142
+ const expiredCap = { ...capability, expiresAt: new Date(Date.now() - 1000).toISOString() };
143
+ const expRes = await hands.executeAction(action, expiredCap);
144
+ expect(expRes.success).toBe(false);
145
+ expect(expRes.error).toContain('Invalid or forged AuthorizationCapability signature');
146
+ });
147
+ });
148
+ describe('P1 — Recursive Schema Validation & Prototype Pollution Defense', () => {
149
+ let hands;
150
+ beforeEach(() => {
151
+ hands = new index_1.DefaultHandsOrgan({ secretKey });
152
+ hands.registerTool({
153
+ definition: {
154
+ name: 'update_user',
155
+ description: 'Update user',
156
+ inputSchema: {
157
+ type: 'object',
158
+ required: ['user'],
159
+ properties: {
160
+ user: {
161
+ type: 'object',
162
+ required: ['id', 'role'],
163
+ properties: {
164
+ id: { type: 'number' },
165
+ role: { type: 'string', enum: ['admin', 'operator', 'owner'] },
166
+ },
167
+ },
168
+ tags: {
169
+ type: 'array',
170
+ items: {
171
+ type: 'object',
172
+ required: ['tagId'],
173
+ properties: {
174
+ tagId: { type: 'number' },
175
+ },
176
+ },
177
+ },
178
+ },
179
+ },
180
+ },
181
+ execute: async (p) => p,
182
+ });
183
+ engine.registerToolDefinition({
184
+ name: 'update_user',
185
+ description: 'Update user',
186
+ inputSchema: {},
187
+ riskLevel: 'LOW',
188
+ });
189
+ });
190
+ it('rejects invalid nested object types and missing nested required fields', async () => {
191
+ const invalidAction = {
192
+ actionId: 'act-nest-1',
193
+ toolName: 'update_user',
194
+ parameters: {
195
+ user: { id: 'not-a-number', role: 'admin' }, // id should be number
196
+ },
197
+ context: sampleContext,
198
+ };
199
+ const { capability } = await engine.evaluateAction(invalidAction);
200
+ const res = await hands.executeAction(invalidAction, capability);
201
+ expect(res.success).toBe(false);
202
+ expect(res.lifecycle).toBe('REJECTED');
203
+ expect(res.error).toContain('user.id');
204
+ expect(res.error).toContain('expected type number');
205
+ });
206
+ it('rejects invalid array item types at specific index paths', async () => {
207
+ const invalidArrayAction = {
208
+ actionId: 'act-arr-1',
209
+ toolName: 'update_user',
210
+ parameters: {
211
+ user: { id: 100, role: 'owner' },
212
+ tags: [
213
+ { tagId: 1 },
214
+ { tagId: 'invalid-tag-id' }, // index 1 is invalid
215
+ ],
216
+ },
217
+ context: sampleContext,
218
+ };
219
+ const { capability } = await engine.evaluateAction(invalidArrayAction);
220
+ const res = await hands.executeAction(invalidArrayAction, capability);
221
+ expect(res.success).toBe(false);
222
+ expect(res.lifecycle).toBe('REJECTED');
223
+ expect(res.error).toContain('tags[1].tagId');
224
+ });
225
+ it('detects and rejects prototype pollution attempts (__proto__, constructor, prototype)', async () => {
226
+ const parsedProtoPayload = JSON.parse('{"user":{"id":1,"role":"admin"},"__proto__":{"isAdmin":true}}');
227
+ const protoPollutionAction = {
228
+ actionId: 'act-proto-1',
229
+ toolName: 'update_user',
230
+ parameters: parsedProtoPayload,
231
+ context: sampleContext,
232
+ };
233
+ const { capability } = await engine.evaluateAction(protoPollutionAction);
234
+ const res = await hands.executeAction(protoPollutionAction, capability);
235
+ expect(res.success).toBe(false);
236
+ expect(res.lifecycle).toBe('REJECTED');
237
+ expect(res.error).toContain('Forbidden prototype pollution key "__proto__"');
238
+ });
239
+ });
240
+ describe('P1 — Idempotency and Concurrency Reservation', () => {
241
+ it('prevents concurrent duplicate execution with reservation conflict', async () => {
242
+ let resolveSlowTool;
243
+ const slowExecutionPromise = new Promise((r) => { resolveSlowTool = r; });
244
+ const hands = new index_1.DefaultHandsOrgan({ secretKey });
245
+ hands.registerTool({
246
+ definition: { name: 'long_task', inputSchema: {}, description: 'long task' },
247
+ execute: async () => slowExecutionPromise,
248
+ });
249
+ engine.registerToolDefinition({ name: 'long_task', inputSchema: {}, description: 'long task', riskLevel: 'LOW' });
250
+ const action = {
251
+ actionId: 'act-race-1',
252
+ toolName: 'long_task',
253
+ parameters: {},
254
+ context: sampleContext,
255
+ executionId: 'exec-concurrent-test-99',
256
+ };
257
+ const { capability } = await engine.evaluateAction(action);
258
+ // Start first execution (which stays EXECUTING)
259
+ const firstPromise = hands.executeAction(action, capability);
260
+ // Concurrently start second execution with same executionId
261
+ const secondRes = await hands.executeAction(action, capability);
262
+ expect(secondRes.success).toBe(false);
263
+ expect(secondRes.error).toContain('Concurrent execution');
264
+ // Finish first execution
265
+ resolveSlowTool({ done: true });
266
+ const firstRes = await firstPromise;
267
+ expect(firstRes.success).toBe(true);
268
+ expect(firstRes.lifecycle).toBe('COMPLETED');
269
+ });
270
+ it('deduplicates/returns cached result on replay of completed executionId', async () => {
271
+ let runCount = 0;
272
+ const hands = new index_1.DefaultHandsOrgan({ secretKey });
273
+ hands.registerTool({
274
+ definition: { name: 'idempotent_task', inputSchema: {}, description: 'idempotent' },
275
+ execute: async () => { runCount++; return { runCount, status: 'done' }; },
276
+ });
277
+ engine.registerToolDefinition({ name: 'idempotent_task', inputSchema: {}, description: 'idempotent', riskLevel: 'LOW' });
278
+ const action = {
279
+ actionId: 'act-idem-1',
280
+ toolName: 'idempotent_task',
281
+ parameters: { data: 'test' },
282
+ context: sampleContext,
283
+ executionId: 'exec-replay-test-101',
284
+ };
285
+ const { capability } = await engine.evaluateAction(action);
286
+ // First run
287
+ const res1 = await hands.executeAction(action, capability);
288
+ expect(res1.success).toBe(true);
289
+ expect(res1.lifecycle).toBe('COMPLETED');
290
+ expect(runCount).toBe(1);
291
+ // Replay run with identical capability
292
+ const res2 = await hands.executeAction(action, capability);
293
+ expect(res2.success).toBe(true);
294
+ expect(res2.lifecycle).toBe('COMPLETED');
295
+ expect(runCount).toBe(1); // Did not re-execute side effect!
296
+ expect(res2.result.status).toBe('done');
297
+ });
298
+ it('cancels the execution when timeout occurs even if caller supplies an external AbortSignal', async () => {
299
+ let abortedSignal = false;
300
+ const hands = new index_1.DefaultHandsOrgan({ secretKey });
301
+ hands.registerTool({
302
+ definition: { name: 'slow_task', inputSchema: {}, description: 'slow task', timeoutMs: 50 },
303
+ execute: async (_, signal) => {
304
+ return new Promise((resolve, reject) => {
305
+ signal?.addEventListener('abort', () => {
306
+ abortedSignal = true;
307
+ const err = new Error('aborted');
308
+ err.name = 'AbortError';
309
+ reject(err);
310
+ });
311
+ });
312
+ },
313
+ });
314
+ engine.registerToolDefinition({ name: 'slow_task', inputSchema: {}, description: 'slow task', riskLevel: 'LOW' });
315
+ const action = {
316
+ actionId: 'act-timeout-signal',
317
+ toolName: 'slow_task',
318
+ parameters: {},
319
+ context: sampleContext,
320
+ executionId: 'exec-timeout-signal-1',
321
+ };
322
+ const { capability } = await engine.evaluateAction(action);
323
+ const externalController = new AbortController();
324
+ const res = await hands.executeAction(action, capability, {
325
+ signal: externalController.signal,
326
+ timeoutMs: 30,
327
+ });
328
+ expect(res.success).toBe(false);
329
+ expect(res.lifecycle).toBe('TIMED_OUT');
330
+ expect(abortedSignal).toBe(true);
331
+ });
332
+ it('registers and executes audited Life DB tools with signed capability', async () => {
333
+ const mockKnowledge = {
334
+ entities: {
335
+ saveEntity: jest.fn(async (ent) => ent),
336
+ },
337
+ events: {
338
+ addEvent: jest.fn(async (ev) => ev),
339
+ },
340
+ schedule: {
341
+ saveItem: jest.fn(async (sch) => sch),
342
+ },
343
+ tasks: {
344
+ saveTask: jest.fn(async (tsk) => tsk),
345
+ },
346
+ };
347
+ const hands = new index_1.DefaultHandsOrgan({ secretKey, knowledge: mockKnowledge });
348
+ const tools = await hands.listTools();
349
+ expect(tools.length).toBeGreaterThanOrEqual(4);
350
+ for (const t of tools) {
351
+ engine.registerToolDefinition(t);
352
+ }
353
+ // Execute life:save_entity
354
+ const saveAction = {
355
+ actionId: 'act-save-ent-1',
356
+ toolName: 'life:save_entity',
357
+ parameters: {
358
+ name: 'MacBook Pro',
359
+ entityType: 'hardware',
360
+ domain: 'workstation',
361
+ properties: { ram: '64GB' },
362
+ },
363
+ context: sampleContext,
364
+ executionId: 'exec-save-ent-1',
365
+ };
366
+ const { capability: cap1 } = await engine.evaluateAction(saveAction);
367
+ expect(cap1).toBeDefined();
368
+ const res1 = await hands.executeAction(saveAction, cap1);
369
+ expect(res1.success).toBe(true);
370
+ expect(res1.lifecycle).toBe('COMPLETED');
371
+ expect(mockKnowledge.entities.saveEntity).toHaveBeenCalledTimes(1);
372
+ // Execute life:log_event
373
+ const logAction = {
374
+ actionId: 'act-log-evt-1',
375
+ toolName: 'life:log_event',
376
+ parameters: {
377
+ stream: 'workout',
378
+ metricValue: 10,
379
+ metadata: { unit: 'km' },
380
+ },
381
+ context: sampleContext,
382
+ executionId: 'exec-log-evt-1',
383
+ };
384
+ const { capability: cap2 } = await engine.evaluateAction(logAction);
385
+ const res2 = await hands.executeAction(logAction, cap2);
386
+ expect(res2.success).toBe(true);
387
+ expect(res2.lifecycle).toBe('COMPLETED');
388
+ expect(mockKnowledge.events.addEvent).toHaveBeenCalledTimes(1);
389
+ // Execute life:update_task
390
+ const taskAction = {
391
+ actionId: 'act-task-1',
392
+ toolName: 'life:update_task',
393
+ parameters: {
394
+ title: 'Implement Life DB Primitives',
395
+ status: 'completed',
396
+ },
397
+ context: sampleContext,
398
+ executionId: 'exec-task-1',
399
+ };
400
+ const { capability: cap3 } = await engine.evaluateAction(taskAction);
401
+ const res3 = await hands.executeAction(taskAction, cap3);
402
+ expect(res3.success).toBe(true);
403
+ expect(res3.lifecycle).toBe('COMPLETED');
404
+ expect(mockKnowledge.tasks.saveTask).toHaveBeenCalledTimes(1);
405
+ });
406
+ });
407
+ });
@@ -0,0 +1,9 @@
1
+ import { ToolHandler } from './index';
2
+ export interface LifeToolsOptions {
3
+ companionId?: string;
4
+ }
5
+ /**
6
+ * Creates audited action tool handlers for mutating the sovereign Life Database
7
+ * in accordance with Siduri-X Action Policy and Truth Gate governance.
8
+ */
9
+ export declare function createLifeTools(knowledge: any, options?: LifeToolsOptions): ToolHandler[];
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLifeTools = createLifeTools;
4
+ /**
5
+ * Creates audited action tool handlers for mutating the sovereign Life Database
6
+ * in accordance with Siduri-X Action Policy and Truth Gate governance.
7
+ */
8
+ function createLifeTools(knowledge, options = {}) {
9
+ const getCompId = (params) => {
10
+ return params.companionId || options.companionId || 'default';
11
+ };
12
+ const handlers = [];
13
+ // 1. life:save_entity
14
+ const saveEntityDef = {
15
+ name: 'life:save_entity',
16
+ providerId: 'life',
17
+ description: 'Create or update an entity (item, contact, place, preference, hardware) in the sovereign Life DB',
18
+ riskLevel: 'LOW',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ id: { type: 'string', description: 'Optional unique entity ID' },
23
+ companionId: { type: 'string', description: 'Optional companion ID' },
24
+ name: { type: 'string', description: 'Entity name' },
25
+ entityType: { type: 'string', description: 'Entity type (e.g. inventory, contact, place, hardware)' },
26
+ domain: { type: 'string', description: 'Domain (e.g. gaming, work, personal, social)' },
27
+ properties: { type: 'object', description: 'Arbitrary entity properties/metadata' },
28
+ },
29
+ required: ['name'],
30
+ },
31
+ };
32
+ const saveEntityHandler = {
33
+ definition: saveEntityDef,
34
+ execute: async (parameters, _signal) => {
35
+ const compId = getCompId(parameters);
36
+ const name = String(parameters.name || 'Unnamed');
37
+ const entityType = String(parameters.entityType || 'entity');
38
+ const domain = String(parameters.domain || 'general');
39
+ const properties = parameters.properties || {};
40
+ const id = parameters.id || `ent-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
41
+ if (knowledge.entities && typeof knowledge.entities.saveEntity === 'function') {
42
+ await knowledge.entities.saveEntity({
43
+ id,
44
+ companionId: compId,
45
+ entityType,
46
+ domain,
47
+ name,
48
+ properties,
49
+ updatedAt: new Date().toISOString(),
50
+ });
51
+ return { success: true, id, entityType, name, domain };
52
+ }
53
+ else if (knowledge.inventory && typeof knowledge.inventory.saveItem === 'function') {
54
+ await knowledge.inventory.saveItem({
55
+ id,
56
+ companionId: compId,
57
+ domain,
58
+ entityName: name,
59
+ properties,
60
+ updatedAt: new Date().toISOString(),
61
+ });
62
+ return { success: true, id, name, domain };
63
+ }
64
+ throw new Error('Knowledge organ does not support saving entities or inventory');
65
+ },
66
+ };
67
+ handlers.push(saveEntityHandler);
68
+ // 2. life:log_event
69
+ const logEventDef = {
70
+ name: 'life:log_event',
71
+ providerId: 'life',
72
+ description: 'Log an event or metric to the sovereign Life DB time-series stream (finance, health, workout, habit)',
73
+ riskLevel: 'LOW',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ id: { type: 'string', description: 'Optional unique event ID' },
78
+ companionId: { type: 'string', description: 'Optional companion ID' },
79
+ stream: { type: 'string', description: 'Event stream name (finance, health, workout, habit, sleep)' },
80
+ metricValue: { type: 'number', description: 'Numerical metric (dollar amount, duration, weight, distance)' },
81
+ metadata: { type: 'object', description: 'Context metadata (category, currency, unit, notes)' },
82
+ },
83
+ required: ['stream'],
84
+ },
85
+ };
86
+ const logEventHandler = {
87
+ definition: logEventDef,
88
+ execute: async (parameters, _signal) => {
89
+ const compId = getCompId(parameters);
90
+ const stream = String(parameters.stream || 'telemetry');
91
+ const metricValue = typeof parameters.metricValue === 'number' ? parameters.metricValue : undefined;
92
+ const metadata = parameters.metadata || {};
93
+ const id = parameters.id || `evt-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
94
+ const timestamp = parameters.timestamp || new Date().toISOString();
95
+ if (knowledge.events && typeof knowledge.events.addEvent === 'function') {
96
+ await knowledge.events.addEvent({
97
+ id,
98
+ companionId: compId,
99
+ stream,
100
+ timestamp,
101
+ metricValue,
102
+ metadata,
103
+ });
104
+ return { success: true, id, stream, metricValue, timestamp };
105
+ }
106
+ else if (stream === 'finance' && knowledge.finance && typeof knowledge.finance.addEntry === 'function') {
107
+ await knowledge.finance.addEntry({
108
+ id,
109
+ companionId: compId,
110
+ category: metadata.category || 'general',
111
+ amount: metricValue || 0,
112
+ currency: metadata.currency || 'USD',
113
+ timestamp,
114
+ metadata,
115
+ });
116
+ return { success: true, id, stream: 'finance', amount: metricValue };
117
+ }
118
+ throw new Error('Knowledge organ does not support event logging');
119
+ },
120
+ };
121
+ handlers.push(logEventHandler);
122
+ // 3. life:upsert_schedule
123
+ const upsertScheduleDef = {
124
+ name: 'life:upsert_schedule',
125
+ providerId: 'life',
126
+ description: 'Add or update a schedule event or commitment in the sovereign Life DB',
127
+ riskLevel: 'LOW',
128
+ inputSchema: {
129
+ type: 'object',
130
+ properties: {
131
+ id: { type: 'string', description: 'Optional unique schedule item ID' },
132
+ companionId: { type: 'string', description: 'Optional companion ID' },
133
+ title: { type: 'string', description: 'Event title' },
134
+ startTime: { type: 'string', description: 'Start time in ISO format' },
135
+ endTime: { type: 'string', description: 'Optional end time in ISO format' },
136
+ isRecurring: { type: 'boolean', description: 'Whether the event recurs' },
137
+ status: { type: 'string', description: 'Status (e.g. active, completed, cancelled)' },
138
+ },
139
+ required: ['title', 'startTime'],
140
+ },
141
+ };
142
+ const upsertScheduleHandler = {
143
+ definition: upsertScheduleDef,
144
+ execute: async (parameters, _signal) => {
145
+ const compId = getCompId(parameters);
146
+ const title = String(parameters.title || 'Untitled Event');
147
+ const startTime = String(parameters.startTime || new Date().toISOString());
148
+ const endTime = parameters.endTime ? String(parameters.endTime) : undefined;
149
+ const isRecurring = Boolean(parameters.isRecurring);
150
+ const status = String(parameters.status || 'active');
151
+ const id = parameters.id || `sch-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
152
+ if (knowledge.schedule && typeof knowledge.schedule.saveItem === 'function') {
153
+ await knowledge.schedule.saveItem({
154
+ id,
155
+ companionId: compId,
156
+ title,
157
+ startTime,
158
+ endTime,
159
+ isRecurring,
160
+ status,
161
+ });
162
+ return { success: true, id, title, startTime, status };
163
+ }
164
+ throw new Error('Knowledge organ does not support schedule management');
165
+ },
166
+ };
167
+ handlers.push(upsertScheduleHandler);
168
+ // 4. life:update_task
169
+ const updateTaskDef = {
170
+ name: 'life:update_task',
171
+ providerId: 'life',
172
+ description: 'Add, update, or complete a task, to-do, or goal in the sovereign Life DB',
173
+ riskLevel: 'LOW',
174
+ inputSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ id: { type: 'string', description: 'Optional unique task ID' },
178
+ companionId: { type: 'string', description: 'Optional companion ID' },
179
+ title: { type: 'string', description: 'Task title or goal description' },
180
+ status: { type: 'string', description: 'Status (backlog, in_progress, completed, cancelled)' },
181
+ priority: { type: 'number', description: 'Priority rank' },
182
+ targetDate: { type: 'string', description: 'Optional target due date' },
183
+ metadata: { type: 'object', description: 'Optional metadata' },
184
+ },
185
+ required: ['title'],
186
+ },
187
+ };
188
+ const updateTaskHandler = {
189
+ definition: updateTaskDef,
190
+ execute: async (parameters, _signal) => {
191
+ const compId = getCompId(parameters);
192
+ const title = String(parameters.title || 'Untitled Task');
193
+ const status = String(parameters.status || 'backlog');
194
+ const priority = typeof parameters.priority === 'number' ? parameters.priority : 0;
195
+ const targetDate = parameters.targetDate ? String(parameters.targetDate) : undefined;
196
+ const metadata = parameters.metadata || undefined;
197
+ const id = parameters.id || `tsk-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
198
+ if (knowledge.tasks && typeof knowledge.tasks.saveTask === 'function') {
199
+ await knowledge.tasks.saveTask({
200
+ id,
201
+ companionId: compId,
202
+ title,
203
+ status,
204
+ priority,
205
+ targetDate,
206
+ metadata,
207
+ updatedAt: new Date().toISOString(),
208
+ });
209
+ return { success: true, id, title, status, priority };
210
+ }
211
+ throw new Error('Knowledge organ does not support task management');
212
+ },
213
+ };
214
+ handlers.push(updateTaskHandler);
215
+ return handlers;
216
+ }
@@ -0,0 +1 @@
1
+ export {};