vellum 0.2.12 → 0.2.14

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.
Files changed (209) hide show
  1. package/README.md +32 -0
  2. package/bun.lock +2 -2
  3. package/docs/skills.md +4 -4
  4. package/package.json +2 -2
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
  6. package/src/__tests__/app-git-history.test.ts +176 -0
  7. package/src/__tests__/app-git-service.test.ts +169 -0
  8. package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
  9. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
  10. package/src/__tests__/browser-skill-endstate.test.ts +6 -6
  11. package/src/__tests__/call-bridge.test.ts +105 -13
  12. package/src/__tests__/call-domain.test.ts +163 -0
  13. package/src/__tests__/call-orchestrator.test.ts +171 -0
  14. package/src/__tests__/call-routes-http.test.ts +246 -6
  15. package/src/__tests__/channel-approval-routes.test.ts +438 -0
  16. package/src/__tests__/channel-approval.test.ts +266 -0
  17. package/src/__tests__/channel-approvals.test.ts +393 -0
  18. package/src/__tests__/channel-delivery-store.test.ts +447 -0
  19. package/src/__tests__/checker.test.ts +607 -1048
  20. package/src/__tests__/cli.test.ts +1 -56
  21. package/src/__tests__/config-schema.test.ts +402 -5
  22. package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
  23. package/src/__tests__/conflict-policy.test.ts +121 -0
  24. package/src/__tests__/conflict-store.test.ts +2 -0
  25. package/src/__tests__/contacts-tools.test.ts +3 -3
  26. package/src/__tests__/contradiction-checker.test.ts +99 -1
  27. package/src/__tests__/credential-security-invariants.test.ts +22 -6
  28. package/src/__tests__/credential-vault-unit.test.ts +780 -0
  29. package/src/__tests__/elevenlabs-client.test.ts +271 -0
  30. package/src/__tests__/ephemeral-permissions.test.ts +73 -23
  31. package/src/__tests__/filesystem-tools.test.ts +579 -0
  32. package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
  33. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
  34. package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
  35. package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
  36. package/src/__tests__/handlers-slack-config.test.ts +2 -1
  37. package/src/__tests__/handlers-telegram-config.test.ts +855 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +141 -1
  39. package/src/__tests__/hooks-runner.test.ts +6 -2
  40. package/src/__tests__/host-file-edit-tool.test.ts +124 -0
  41. package/src/__tests__/host-file-read-tool.test.ts +62 -0
  42. package/src/__tests__/host-file-write-tool.test.ts +59 -0
  43. package/src/__tests__/host-shell-tool.test.ts +251 -0
  44. package/src/__tests__/ingress-reconcile.test.ts +581 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +100 -41
  46. package/src/__tests__/ipc-validate.test.ts +50 -0
  47. package/src/__tests__/key-migration.test.ts +23 -0
  48. package/src/__tests__/memory-regressions.test.ts +99 -0
  49. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  50. package/src/__tests__/oauth-callback-registry.test.ts +11 -4
  51. package/src/__tests__/playbook-execution.test.ts +502 -0
  52. package/src/__tests__/playbook-tools.test.ts +4 -6
  53. package/src/__tests__/public-ingress-urls.test.ts +34 -0
  54. package/src/__tests__/qdrant-manager.test.ts +267 -0
  55. package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
  56. package/src/__tests__/recurrence-engine.test.ts +9 -0
  57. package/src/__tests__/recurrence-types.test.ts +8 -0
  58. package/src/__tests__/registry.test.ts +1 -1
  59. package/src/__tests__/runtime-runs.test.ts +1 -25
  60. package/src/__tests__/schedule-store.test.ts +16 -14
  61. package/src/__tests__/schedule-tools.test.ts +83 -0
  62. package/src/__tests__/scheduler-recurrence.test.ts +111 -10
  63. package/src/__tests__/secret-allowlist.test.ts +18 -17
  64. package/src/__tests__/secret-ingress-handler.test.ts +11 -0
  65. package/src/__tests__/secret-scanner.test.ts +43 -0
  66. package/src/__tests__/session-conflict-gate.test.ts +442 -6
  67. package/src/__tests__/session-init.benchmark.test.ts +3 -0
  68. package/src/__tests__/session-process-bridge.test.ts +242 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -1
  70. package/src/__tests__/shell-identity.test.ts +256 -0
  71. package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
  72. package/src/__tests__/subagent-tools.test.ts +637 -54
  73. package/src/__tests__/task-management-tools.test.ts +936 -0
  74. package/src/__tests__/task-runner.test.ts +2 -2
  75. package/src/__tests__/terminal-tools.test.ts +840 -0
  76. package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
  77. package/src/__tests__/tool-executor.test.ts +85 -151
  78. package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
  79. package/src/__tests__/trust-store.test.ts +28 -453
  80. package/src/__tests__/twilio-provider.test.ts +153 -3
  81. package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
  82. package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
  83. package/src/__tests__/twilio-routes.test.ts +17 -262
  84. package/src/__tests__/twitter-auth-handler.test.ts +2 -1
  85. package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
  86. package/src/__tests__/twitter-cli-routing.test.ts +252 -0
  87. package/src/__tests__/twitter-oauth-client.test.ts +209 -0
  88. package/src/__tests__/workspace-policy.test.ts +213 -0
  89. package/src/calls/call-bridge.ts +92 -19
  90. package/src/calls/call-domain.ts +157 -5
  91. package/src/calls/call-orchestrator.ts +96 -8
  92. package/src/calls/call-store.ts +6 -0
  93. package/src/calls/elevenlabs-client.ts +97 -0
  94. package/src/calls/elevenlabs-config.ts +31 -0
  95. package/src/calls/twilio-provider.ts +91 -0
  96. package/src/calls/twilio-routes.ts +50 -6
  97. package/src/calls/types.ts +3 -1
  98. package/src/calls/voice-quality.ts +114 -0
  99. package/src/cli/twitter.ts +200 -21
  100. package/src/cli.ts +1 -20
  101. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
  102. package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
  103. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
  104. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  105. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
  106. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  107. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
  108. package/src/config/bundled-skills/phone-calls/SKILL.md +207 -19
  109. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
  110. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
  111. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
  112. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
  113. package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
  114. package/src/config/bundled-skills/twitter/SKILL.md +103 -17
  115. package/src/config/defaults.ts +26 -2
  116. package/src/config/schema.ts +178 -9
  117. package/src/config/types.ts +3 -0
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
  119. package/src/daemon/assistant-attachments.ts +4 -2
  120. package/src/daemon/handlers/apps.ts +69 -0
  121. package/src/daemon/handlers/config.ts +543 -24
  122. package/src/daemon/handlers/index.ts +1 -0
  123. package/src/daemon/handlers/sessions.ts +22 -6
  124. package/src/daemon/handlers/shared.ts +2 -1
  125. package/src/daemon/handlers/skills.ts +5 -20
  126. package/src/daemon/ipc-contract-inventory.json +28 -0
  127. package/src/daemon/ipc-contract.ts +168 -10
  128. package/src/daemon/ipc-validate.ts +17 -0
  129. package/src/daemon/lifecycle.ts +2 -0
  130. package/src/daemon/server.ts +78 -72
  131. package/src/daemon/session-attachments.ts +1 -1
  132. package/src/daemon/session-conflict-gate.ts +62 -6
  133. package/src/daemon/session-notifiers.ts +1 -1
  134. package/src/daemon/session-process.ts +62 -3
  135. package/src/daemon/session-tool-setup.ts +1 -2
  136. package/src/daemon/tls-certs.ts +189 -0
  137. package/src/daemon/video-thumbnail.ts +5 -3
  138. package/src/hooks/manager.ts +5 -9
  139. package/src/memory/app-git-service.ts +295 -0
  140. package/src/memory/app-store.ts +21 -0
  141. package/src/memory/conflict-intent.ts +47 -4
  142. package/src/memory/conflict-policy.ts +73 -0
  143. package/src/memory/conflict-store.ts +9 -1
  144. package/src/memory/contradiction-checker.ts +28 -0
  145. package/src/memory/conversation-key-store.ts +15 -0
  146. package/src/memory/db.ts +81 -0
  147. package/src/memory/embedding-local.ts +3 -13
  148. package/src/memory/external-conversation-store.ts +234 -0
  149. package/src/memory/job-handlers/conflict.ts +22 -2
  150. package/src/memory/jobs-worker.ts +67 -28
  151. package/src/memory/runs-store.ts +54 -7
  152. package/src/memory/schema.ts +20 -0
  153. package/src/messaging/provider.ts +9 -0
  154. package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
  155. package/src/messaging/providers/telegram-bot/client.ts +104 -0
  156. package/src/messaging/providers/telegram-bot/types.ts +15 -0
  157. package/src/messaging/registry.ts +1 -0
  158. package/src/permissions/checker.ts +48 -44
  159. package/src/permissions/defaults.ts +11 -0
  160. package/src/permissions/prompter.ts +0 -4
  161. package/src/permissions/shell-identity.ts +227 -0
  162. package/src/permissions/trust-store.ts +76 -53
  163. package/src/permissions/types.ts +0 -19
  164. package/src/permissions/workspace-policy.ts +114 -0
  165. package/src/providers/retry.ts +12 -37
  166. package/src/runtime/assistant-event-hub.ts +41 -4
  167. package/src/runtime/channel-approval-parser.ts +60 -0
  168. package/src/runtime/channel-approval-types.ts +71 -0
  169. package/src/runtime/channel-approvals.ts +145 -0
  170. package/src/runtime/gateway-client.ts +16 -0
  171. package/src/runtime/http-server.ts +29 -9
  172. package/src/runtime/routes/call-routes.ts +52 -2
  173. package/src/runtime/routes/channel-routes.ts +296 -16
  174. package/src/runtime/routes/conversation-routes.ts +12 -5
  175. package/src/runtime/routes/events-routes.ts +97 -28
  176. package/src/runtime/routes/run-routes.ts +2 -7
  177. package/src/runtime/run-orchestrator.ts +0 -3
  178. package/src/schedule/recurrence-engine.ts +26 -2
  179. package/src/schedule/recurrence-types.ts +1 -1
  180. package/src/schedule/schedule-store.ts +12 -3
  181. package/src/security/secret-scanner.ts +7 -0
  182. package/src/tasks/ephemeral-permissions.ts +0 -2
  183. package/src/tasks/task-scheduler.ts +2 -1
  184. package/src/tools/calls/call-start.ts +8 -0
  185. package/src/tools/execution-target.ts +21 -0
  186. package/src/tools/execution-timeout.ts +49 -0
  187. package/src/tools/executor.ts +6 -135
  188. package/src/tools/network/web-search.ts +9 -32
  189. package/src/tools/policy-context.ts +29 -0
  190. package/src/tools/schedule/update.ts +8 -1
  191. package/src/tools/terminal/parser.ts +16 -18
  192. package/src/tools/types.ts +4 -11
  193. package/src/twitter/oauth-client.ts +102 -0
  194. package/src/twitter/router.ts +101 -0
  195. package/src/util/debounce.ts +88 -0
  196. package/src/util/network-info.ts +47 -0
  197. package/src/util/platform.ts +29 -4
  198. package/src/util/promise-guard.ts +37 -0
  199. package/src/util/retry.ts +98 -0
  200. package/src/util/truncate.ts +1 -1
  201. package/src/workspace/git-service.ts +129 -112
  202. package/src/tools/contacts/contact-merge.ts +0 -55
  203. package/src/tools/contacts/contact-search.ts +0 -58
  204. package/src/tools/contacts/contact-upsert.ts +0 -64
  205. package/src/tools/playbooks/index.ts +0 -4
  206. package/src/tools/playbooks/playbook-create.ts +0 -96
  207. package/src/tools/playbooks/playbook-delete.ts +0 -52
  208. package/src/tools/playbooks/playbook-list.ts +0 -74
  209. package/src/tools/playbooks/playbook-update.ts +0 -111
@@ -148,16 +148,10 @@ describe('Trust Store', () => {
148
148
  expect(userRules[1].decision).toBe('allow');
149
149
  });
150
150
 
151
- test('accepts principal and executionTarget options and persists them', () => {
151
+ test('accepts executionTarget option and persists it', () => {
152
152
  const rule = addRule('skill_tool', 'skill_tool:*', '/tmp', 'allow', 100, {
153
- principalKind: 'skill',
154
- principalId: 'my-skill-42',
155
- principalVersion: 'sha256-abc123',
156
153
  executionTarget: 'sandbox',
157
154
  });
158
- expect(rule.principalKind).toBe('skill');
159
- expect(rule.principalId).toBe('my-skill-42');
160
- expect(rule.principalVersion).toBe('sha256-abc123');
161
155
  expect(rule.executionTarget).toBe('sandbox');
162
156
 
163
157
  // Verify persistence to disk
@@ -165,24 +159,15 @@ describe('Trust Store', () => {
165
159
  const rules = getAllRules();
166
160
  const found = rules.find((r) => r.id === rule.id);
167
161
  expect(found).toBeDefined();
168
- expect(found!.principalKind).toBe('skill');
169
- expect(found!.principalId).toBe('my-skill-42');
170
- expect(found!.principalVersion).toBe('sha256-abc123');
171
162
  expect(found!.executionTarget).toBe('sandbox');
172
163
  });
173
164
 
174
- test('accepts all contextual options together (principal, target, allowHighRisk)', () => {
165
+ test('accepts all contextual options together (target, allowHighRisk)', () => {
175
166
  const rule = addRule('risky_tool', 'risky_tool:*', 'everywhere', 'allow', 100, {
176
167
  allowHighRisk: true,
177
- principalKind: 'skill',
178
- principalId: 'dangerous-skill',
179
- principalVersion: 'sha256-deadbeef',
180
168
  executionTarget: 'host',
181
169
  });
182
170
  expect(rule.allowHighRisk).toBe(true);
183
- expect(rule.principalKind).toBe('skill');
184
- expect(rule.principalId).toBe('dangerous-skill');
185
- expect(rule.principalVersion).toBe('sha256-deadbeef');
186
171
  expect(rule.executionTarget).toBe('host');
187
172
 
188
173
  // Verify on disk
@@ -190,26 +175,17 @@ describe('Trust Store', () => {
190
175
  const diskRule = raw.rules.find((r: { id: string }) => r.id === rule.id);
191
176
  expect(diskRule).toBeDefined();
192
177
  expect(diskRule.allowHighRisk).toBe(true);
193
- expect(diskRule.principalKind).toBe('skill');
194
- expect(diskRule.principalId).toBe('dangerous-skill');
195
- expect(diskRule.principalVersion).toBe('sha256-deadbeef');
196
178
  expect(diskRule.executionTarget).toBe('host');
197
179
  });
198
180
 
199
- test('addRule without principal options does not set principal fields', () => {
181
+ test('addRule without options does not set optional fields', () => {
200
182
  const rule = addRule('bash', 'echo *', '/tmp');
201
- expect(rule.principalKind).toBeUndefined();
202
- expect(rule.principalId).toBeUndefined();
203
- expect(rule.principalVersion).toBeUndefined();
204
183
  expect(rule.executionTarget).toBeUndefined();
205
184
 
206
185
  // Verify on disk
207
186
  const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
208
187
  const diskRule = raw.rules.find((r: { id: string }) => r.id === rule.id);
209
188
  expect(diskRule).toBeDefined();
210
- expect(diskRule).not.toHaveProperty('principalKind');
211
- expect(diskRule).not.toHaveProperty('principalId');
212
- expect(diskRule).not.toHaveProperty('principalVersion');
213
189
  expect(diskRule).not.toHaveProperty('executionTarget');
214
190
  });
215
191
  });
@@ -741,6 +717,7 @@ describe('Trust Store', () => {
741
717
  'host_file_edit',
742
718
  'host_file_read',
743
719
  'host_file_write',
720
+ 'memory_search',
744
721
  'scaffold_managed_skill',
745
722
  'skill_load',
746
723
  'ui_dismiss',
@@ -1025,12 +1002,9 @@ describe('Trust Store', () => {
1025
1002
  // ── trust rule schema v3 (PR 14) ──────────────────────────────
1026
1003
 
1027
1004
  describe('trust rule schema v3 (PR 14)', () => {
1028
- test('new rules can include principal fields', () => {
1005
+ test('new rules can include v3 optional fields', () => {
1029
1006
  const rule = addRule('bash', 'git *', '/tmp');
1030
- // Manually set v3 principal fields on the rule and persist
1031
- rule.principalKind = 'skill';
1032
- rule.principalId = 'my-skill';
1033
- rule.principalVersion = 'abc123';
1007
+ // Manually set v3 optional fields on the rule and persist
1034
1008
  rule.executionTarget = '/usr/local/bin/node';
1035
1009
  rule.allowHighRisk = true;
1036
1010
  // Re-persist the updated rules
@@ -1044,9 +1018,6 @@ describe('Trust Store', () => {
1044
1018
  const reloaded = getAllRules();
1045
1019
  const found = reloaded.find((r) => r.id === rule.id);
1046
1020
  expect(found).toBeDefined();
1047
- expect(found!.principalKind).toBe('skill');
1048
- expect(found!.principalId).toBe('my-skill');
1049
- expect(found!.principalVersion).toBe('abc123');
1050
1021
  expect(found!.executionTarget).toBe('/usr/local/bin/node');
1051
1022
  expect(found!.allowHighRisk).toBe(true);
1052
1023
  });
@@ -1071,7 +1042,7 @@ describe('Trust Store', () => {
1071
1042
  expect(data.version).toBe(3);
1072
1043
  });
1073
1044
 
1074
- test('v2 rules survive v3 migration with no principal fields', () => {
1045
+ test('v2 rules survive v3 migration with no v3-only fields', () => {
1075
1046
  mkdirSync(dirname(trustPath), { recursive: true });
1076
1047
  writeFileSync(trustPath, JSON.stringify({
1077
1048
  version: 2,
@@ -1104,10 +1075,7 @@ describe('Trust Store', () => {
1104
1075
  expect(ruleB).toBeDefined();
1105
1076
  expect(ruleA!.pattern).toBe('git *');
1106
1077
  expect(ruleB!.decision).toBe('deny');
1107
- // No principal fields should be present
1108
- expect(ruleA).not.toHaveProperty('principalKind');
1109
- expect(ruleA).not.toHaveProperty('principalId');
1110
- expect(ruleA).not.toHaveProperty('principalVersion');
1078
+ // No v3-only fields should be present
1111
1079
  expect(ruleA).not.toHaveProperty('executionTarget');
1112
1080
  expect(ruleA).not.toHaveProperty('allowHighRisk');
1113
1081
  });
@@ -1253,25 +1221,22 @@ describe('Trust Store', () => {
1253
1221
  expect(userRules).toHaveLength(1);
1254
1222
  });
1255
1223
 
1256
- test('v3 file with principal fields is loaded correctly without re-migration', () => {
1224
+ test('v3 file with optional fields is loaded correctly without re-migration', () => {
1257
1225
  mkdirSync(dirname(trustPath), { recursive: true });
1258
1226
  const v3Rules = [
1259
1227
  {
1260
- id: 'v3-with-principal',
1228
+ id: 'v3-with-options',
1261
1229
  tool: 'bash',
1262
1230
  pattern: 'skill-cmd *',
1263
1231
  scope: '/tmp',
1264
1232
  decision: 'allow',
1265
1233
  priority: 100,
1266
1234
  createdAt: 7000,
1267
- principalKind: 'skill',
1268
- principalId: 'my-skill',
1269
- principalVersion: 'sha256-abc',
1270
1235
  executionTarget: '/usr/bin/node',
1271
1236
  allowHighRisk: false,
1272
1237
  },
1273
1238
  {
1274
- id: 'v3-without-principal',
1239
+ id: 'v3-without-options',
1275
1240
  tool: 'bash',
1276
1241
  pattern: 'git *',
1277
1242
  scope: '/tmp',
@@ -1284,23 +1249,19 @@ describe('Trust Store', () => {
1284
1249
  clearCache();
1285
1250
  const rules = getAllRules();
1286
1251
 
1287
- // Rule with principal fields should have them preserved
1288
- const withPrincipal = rules.find((r) => r.id === 'v3-with-principal');
1289
- expect(withPrincipal).toBeDefined();
1290
- expect(withPrincipal!.principalKind).toBe('skill');
1291
- expect(withPrincipal!.principalId).toBe('my-skill');
1292
- expect(withPrincipal!.principalVersion).toBe('sha256-abc');
1293
- expect(withPrincipal!.executionTarget).toBe('/usr/bin/node');
1294
- expect(withPrincipal!.allowHighRisk).toBe(false);
1252
+ // Rule with optional fields should have them preserved
1253
+ const withOptions = rules.find((r) => r.id === 'v3-with-options');
1254
+ expect(withOptions).toBeDefined();
1255
+ expect(withOptions!.executionTarget).toBe('/usr/bin/node');
1256
+ expect(withOptions!.allowHighRisk).toBe(false);
1295
1257
 
1296
- // Rule without principal fields should remain without them
1297
- const withoutPrincipal = rules.find((r) => r.id === 'v3-without-principal');
1298
- expect(withoutPrincipal).toBeDefined();
1299
- expect(withoutPrincipal).not.toHaveProperty('principalKind');
1300
- expect(withoutPrincipal).not.toHaveProperty('principalId');
1258
+ // Rule without optional fields should remain without them
1259
+ const withoutOptions = rules.find((r) => r.id === 'v3-without-options');
1260
+ expect(withoutOptions).toBeDefined();
1261
+ expect(withoutOptions).not.toHaveProperty('executionTarget');
1301
1262
  });
1302
1263
 
1303
- test('v2 migration preserves rule meaning exactly — no default principal values added', () => {
1264
+ test('v2 migration preserves rule meaning exactly — no extra fields added', () => {
1304
1265
  mkdirSync(dirname(trustPath), { recursive: true });
1305
1266
  const originalRules = [
1306
1267
  {
@@ -1336,22 +1297,10 @@ describe('Trust Store', () => {
1336
1297
  expect(migrated!.decision).toBe(original.decision);
1337
1298
  expect(migrated!.priority).toBe(original.priority);
1338
1299
  expect(migrated!.createdAt).toBe(original.createdAt);
1339
- // No principal fields were injected by migration
1340
- expect(migrated).not.toHaveProperty('principalKind');
1341
- expect(migrated).not.toHaveProperty('principalId');
1342
- expect(migrated).not.toHaveProperty('principalVersion');
1300
+ // No extra fields were injected by migration
1343
1301
  expect(migrated).not.toHaveProperty('executionTarget');
1344
1302
  expect(migrated).not.toHaveProperty('allowHighRisk');
1345
1303
  }
1346
-
1347
- // Verify disk representation also has no principal fields on user rules
1348
- const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
1349
- for (const original of originalRules) {
1350
- const diskRule = data.rules.find((r: { id: string }) => r.id === original.id);
1351
- expect(diskRule).toBeDefined();
1352
- expect(diskRule).not.toHaveProperty('principalKind');
1353
- expect(diskRule).not.toHaveProperty('principalId');
1354
- }
1355
1304
  });
1356
1305
 
1357
1306
  test('v1 → v3 full migration preserves rules and adds priority', () => {
@@ -1373,54 +1322,15 @@ describe('Trust Store', () => {
1373
1322
  expect(rule).toBeDefined();
1374
1323
  // v1 → v2 adds priority 100
1375
1324
  expect(rule!.priority).toBe(100);
1376
- // v2 → v3 adds no principal fields
1377
- expect(rule).not.toHaveProperty('principalKind');
1378
- expect(rule).not.toHaveProperty('principalId');
1379
1325
  // File should be v3 on disk
1380
1326
  const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
1381
1327
  expect(data.version).toBe(3);
1382
1328
  });
1383
1329
  });
1384
1330
 
1385
- // ── backward compat: addRule without principal options (PR 2/40) ──
1386
- // These tests verify that addRule() without explicit principal options
1387
- // creates wildcard rules. The TrustRule schema *does* support principal
1388
- // and version fields (since PR 14), but they are only set when explicitly
1389
- // provided via the options parameter.
1390
-
1391
- describe('backward compat: addRule without principal options (PR 2/40)', () => {
1392
- test('addRule without principal options creates rules without principal fields', () => {
1393
- const rule = addRule('skill_test_tool', 'skill_test_tool:*', '/tmp');
1394
- expect(rule).not.toHaveProperty('principalKind');
1395
- expect(rule).not.toHaveProperty('principalId');
1396
- expect(rule).not.toHaveProperty('principalVersion');
1397
- expect(rule).not.toHaveProperty('executionTarget');
1398
- expect(rule).not.toHaveProperty('allowHighRisk');
1399
- });
1400
-
1401
- test('findHighestPriorityRule matches without policy context (backward compat)', () => {
1402
- addRule('skill_test_tool', 'skill_test_tool:*', '/tmp', 'allow', 200);
1403
- // Calling without the optional 4th ctx parameter still matches wildcard rules
1404
- const match = findHighestPriorityRule('skill_test_tool', ['skill_test_tool:do-thing'], '/tmp');
1405
- expect(match).not.toBeNull();
1406
- expect(match!.decision).toBe('allow');
1407
- });
1408
-
1409
- test('trust file schema is v3 (rules created without principal fields)', () => {
1410
- addRule('skill_test_tool', 'skill_test_tool:*', '/tmp');
1411
- const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
1412
- expect(raw.version).toBe(3);
1413
- const userRule = raw.rules.find((r: { pattern: string }) => r.pattern === 'skill_test_tool:*');
1414
- expect(userRule).toBeDefined();
1415
- // addRule without principal options doesn't set principal fields
1416
- expect(userRule).not.toHaveProperty('principalVersion');
1417
- expect(userRule).not.toHaveProperty('principalKind');
1418
- });
1419
- });
1420
-
1421
- // ── principal-aware rule matching (PR 16) ──────────────────────
1331
+ // ── executionTarget-aware rule matching ──────────────────────
1422
1332
 
1423
- describe('principal-aware rule matching (PR 16)', () => {
1333
+ describe('executionTarget-aware rule matching', () => {
1424
1334
  /**
1425
1335
  * Helper: write a v3 trust file with the given rules directly to disk,
1426
1336
  * then clear the cache so the next getRules() call picks them up.
@@ -1431,26 +1341,17 @@ describe('Trust Store', () => {
1431
1341
  clearCache();
1432
1342
  }
1433
1343
 
1434
- // ── wildcard semantics (no principal fields on rule) ──────────
1344
+ // ── wildcard semantics (no executionTarget on rule) ──────────
1435
1345
 
1436
- describe('wildcard semantics — rules without principal fields', () => {
1437
- test('rule with no principal fields matches when no context is provided', () => {
1346
+ describe('wildcard semantics — rules without executionTarget', () => {
1347
+ test('rule with no executionTarget matches when no context is provided', () => {
1438
1348
  addRule('bash', 'git *', '/tmp', 'allow', 200);
1439
1349
  const match = findHighestPriorityRule('bash', ['git status'], '/tmp');
1440
1350
  expect(match).not.toBeNull();
1441
1351
  expect(match!.decision).toBe('allow');
1442
1352
  });
1443
1353
 
1444
- test('rule with no principal fields matches any principal context', () => {
1445
- addRule('bash', 'git *', '/tmp', 'allow', 200);
1446
- const match = findHighestPriorityRule('bash', ['git status'], '/tmp', {
1447
- principal: { kind: 'skill', id: 'my-skill', version: 'v1' },
1448
- });
1449
- expect(match).not.toBeNull();
1450
- expect(match!.decision).toBe('allow');
1451
- });
1452
-
1453
- test('rule with no principal fields matches any execution target', () => {
1354
+ test('rule with no executionTarget matches any execution target', () => {
1454
1355
  addRule('bash', 'git *', '/tmp', 'allow', 200);
1455
1356
  const match = findHighestPriorityRule('bash', ['git status'], '/tmp', {
1456
1357
  executionTarget: '/usr/bin/node',
@@ -1458,186 +1359,6 @@ describe('Trust Store', () => {
1458
1359
  expect(match).not.toBeNull();
1459
1360
  expect(match!.decision).toBe('allow');
1460
1361
  });
1461
-
1462
- test('rule with no principal fields matches context with both principal and target', () => {
1463
- addRule('bash', 'npm *', '/tmp', 'allow', 200);
1464
- const match = findHighestPriorityRule('bash', ['npm install'], '/tmp', {
1465
- principal: { kind: 'skill', id: 'builder', version: 'sha256-xyz' },
1466
- executionTarget: '/usr/local/bin/bun',
1467
- });
1468
- expect(match).not.toBeNull();
1469
- });
1470
- });
1471
-
1472
- // ── principalKind matching ────────────────────────────────────
1473
-
1474
- describe('principalKind matching', () => {
1475
- test('rule with principalKind matches when context kind matches', () => {
1476
- seedRules([{
1477
- id: 'pk-match',
1478
- tool: 'bash',
1479
- pattern: 'echo *',
1480
- scope: 'everywhere',
1481
- decision: 'allow',
1482
- priority: 200,
1483
- createdAt: Date.now(),
1484
- principalKind: 'skill',
1485
- }]);
1486
- const match = findHighestPriorityRule('bash', ['echo hello'], '/tmp', {
1487
- principal: { kind: 'skill' },
1488
- });
1489
- expect(match).not.toBeNull();
1490
- expect(match!.id).toBe('pk-match');
1491
- });
1492
-
1493
- test('rule with principalKind does NOT match when context kind differs', () => {
1494
- seedRules([{
1495
- id: 'pk-mismatch',
1496
- tool: 'bash',
1497
- pattern: 'echo *',
1498
- scope: 'everywhere',
1499
- decision: 'allow',
1500
- priority: 200,
1501
- createdAt: Date.now(),
1502
- principalKind: 'skill',
1503
- }]);
1504
- const match = findHighestPriorityRule('bash', ['echo hello'], '/tmp', {
1505
- principal: { kind: 'core' },
1506
- });
1507
- // Should not match the pk-mismatch rule; may still match a default rule
1508
- expect(match === null || match.id !== 'pk-mismatch').toBe(true);
1509
- });
1510
-
1511
- test('rule with principalKind does NOT match when no context is provided', () => {
1512
- seedRules([{
1513
- id: 'pk-no-ctx',
1514
- tool: 'bash',
1515
- pattern: 'echo *',
1516
- scope: 'everywhere',
1517
- decision: 'allow',
1518
- priority: 200,
1519
- createdAt: Date.now(),
1520
- principalKind: 'skill',
1521
- }]);
1522
- const match = findHighestPriorityRule('bash', ['echo hello'], '/tmp');
1523
- expect(match === null || match.id !== 'pk-no-ctx').toBe(true);
1524
- });
1525
- });
1526
-
1527
- // ── principalId matching ──────────────────────────────────────
1528
-
1529
- describe('principalId matching', () => {
1530
- test('rule with principalKind + principalId matches exact principal', () => {
1531
- seedRules([{
1532
- id: 'pid-exact',
1533
- tool: 'bash',
1534
- pattern: 'deploy *',
1535
- scope: 'everywhere',
1536
- decision: 'allow',
1537
- priority: 200,
1538
- createdAt: Date.now(),
1539
- principalKind: 'skill',
1540
- principalId: 'deployer',
1541
- }]);
1542
- const match = findHighestPriorityRule('bash', ['deploy prod'], '/tmp', {
1543
- principal: { kind: 'skill', id: 'deployer' },
1544
- });
1545
- expect(match).not.toBeNull();
1546
- expect(match!.id).toBe('pid-exact');
1547
- });
1548
-
1549
- test('rule with principalId does NOT match different id', () => {
1550
- seedRules([{
1551
- id: 'pid-diff',
1552
- tool: 'bash',
1553
- pattern: 'deploy *',
1554
- scope: 'everywhere',
1555
- decision: 'allow',
1556
- priority: 200,
1557
- createdAt: Date.now(),
1558
- principalKind: 'skill',
1559
- principalId: 'deployer',
1560
- }]);
1561
- const match = findHighestPriorityRule('bash', ['deploy prod'], '/tmp', {
1562
- principal: { kind: 'skill', id: 'other-skill' },
1563
- });
1564
- expect(match === null || match.id !== 'pid-diff').toBe(true);
1565
- });
1566
- });
1567
-
1568
- // ── principalVersion matching ─────────────────────────────────
1569
-
1570
- describe('principalVersion matching', () => {
1571
- test('rule with principalVersion matches exact version', () => {
1572
- seedRules([{
1573
- id: 'pv-exact',
1574
- tool: 'bash',
1575
- pattern: 'build *',
1576
- scope: 'everywhere',
1577
- decision: 'allow',
1578
- priority: 200,
1579
- createdAt: Date.now(),
1580
- principalKind: 'skill',
1581
- principalId: 'builder',
1582
- principalVersion: 'sha256-abc123',
1583
- }]);
1584
- const match = findHighestPriorityRule('bash', ['build all'], '/tmp', {
1585
- principal: { kind: 'skill', id: 'builder', version: 'sha256-abc123' },
1586
- });
1587
- expect(match).not.toBeNull();
1588
- expect(match!.id).toBe('pv-exact');
1589
- });
1590
-
1591
- test('rule with principalVersion does NOT match different version', () => {
1592
- seedRules([{
1593
- id: 'pv-diff',
1594
- tool: 'bash',
1595
- pattern: 'build *',
1596
- scope: 'everywhere',
1597
- decision: 'allow',
1598
- priority: 200,
1599
- createdAt: Date.now(),
1600
- principalKind: 'skill',
1601
- principalId: 'builder',
1602
- principalVersion: 'sha256-abc123',
1603
- }]);
1604
- const match = findHighestPriorityRule('bash', ['build all'], '/tmp', {
1605
- principal: { kind: 'skill', id: 'builder', version: 'sha256-DIFFERENT' },
1606
- });
1607
- expect(match === null || match.id !== 'pv-diff').toBe(true);
1608
- });
1609
-
1610
- test('rule WITHOUT principalVersion matches any version (wildcard)', () => {
1611
- seedRules([{
1612
- id: 'pv-wildcard',
1613
- tool: 'bash',
1614
- pattern: 'build *',
1615
- scope: 'everywhere',
1616
- decision: 'allow',
1617
- priority: 200,
1618
- createdAt: Date.now(),
1619
- principalKind: 'skill',
1620
- principalId: 'builder',
1621
- // no principalVersion — should match any version
1622
- }]);
1623
- const matchV1 = findHighestPriorityRule('bash', ['build all'], '/tmp', {
1624
- principal: { kind: 'skill', id: 'builder', version: 'v1' },
1625
- });
1626
- expect(matchV1).not.toBeNull();
1627
- expect(matchV1!.id).toBe('pv-wildcard');
1628
-
1629
- const matchV2 = findHighestPriorityRule('bash', ['build all'], '/tmp', {
1630
- principal: { kind: 'skill', id: 'builder', version: 'v2' },
1631
- });
1632
- expect(matchV2).not.toBeNull();
1633
- expect(matchV2!.id).toBe('pv-wildcard');
1634
-
1635
- const matchNoVersion = findHighestPriorityRule('bash', ['build all'], '/tmp', {
1636
- principal: { kind: 'skill', id: 'builder' },
1637
- });
1638
- expect(matchNoVersion).not.toBeNull();
1639
- expect(matchNoVersion!.id).toBe('pv-wildcard');
1640
- });
1641
1362
  });
1642
1363
 
1643
1364
  // ── executionTarget matching ──────────────────────────────────
@@ -1703,139 +1424,6 @@ describe('Trust Store', () => {
1703
1424
  });
1704
1425
  });
1705
1426
 
1706
- // ── combined principal + executionTarget ───────────────────────
1707
-
1708
- describe('combined principal + executionTarget matching', () => {
1709
- test('rule with both principal and executionTarget matches when all fields match', () => {
1710
- seedRules([{
1711
- id: 'combo-match',
1712
- tool: 'bash',
1713
- pattern: 'deploy *',
1714
- scope: 'everywhere',
1715
- decision: 'allow',
1716
- priority: 200,
1717
- createdAt: Date.now(),
1718
- principalKind: 'skill',
1719
- principalId: 'deployer',
1720
- principalVersion: 'sha256-abc',
1721
- executionTarget: '/usr/bin/node',
1722
- }]);
1723
- const match = findHighestPriorityRule('bash', ['deploy prod'], '/tmp', {
1724
- principal: { kind: 'skill', id: 'deployer', version: 'sha256-abc' },
1725
- executionTarget: '/usr/bin/node',
1726
- });
1727
- expect(match).not.toBeNull();
1728
- expect(match!.id).toBe('combo-match');
1729
- });
1730
-
1731
- test('rule with both principal and executionTarget fails if principal mismatches', () => {
1732
- seedRules([{
1733
- id: 'combo-bad-principal',
1734
- tool: 'bash',
1735
- pattern: 'deploy *',
1736
- scope: 'everywhere',
1737
- decision: 'allow',
1738
- priority: 200,
1739
- createdAt: Date.now(),
1740
- principalKind: 'skill',
1741
- principalId: 'deployer',
1742
- executionTarget: '/usr/bin/node',
1743
- }]);
1744
- const match = findHighestPriorityRule('bash', ['deploy prod'], '/tmp', {
1745
- principal: { kind: 'skill', id: 'other-skill' },
1746
- executionTarget: '/usr/bin/node',
1747
- });
1748
- expect(match === null || match.id !== 'combo-bad-principal').toBe(true);
1749
- });
1750
-
1751
- test('rule with both principal and executionTarget fails if target mismatches', () => {
1752
- seedRules([{
1753
- id: 'combo-bad-target',
1754
- tool: 'bash',
1755
- pattern: 'deploy *',
1756
- scope: 'everywhere',
1757
- decision: 'allow',
1758
- priority: 200,
1759
- createdAt: Date.now(),
1760
- principalKind: 'skill',
1761
- principalId: 'deployer',
1762
- executionTarget: '/usr/bin/node',
1763
- }]);
1764
- const match = findHighestPriorityRule('bash', ['deploy prod'], '/tmp', {
1765
- principal: { kind: 'skill', id: 'deployer' },
1766
- executionTarget: '/usr/bin/bun',
1767
- });
1768
- expect(match === null || match.id !== 'combo-bad-target').toBe(true);
1769
- });
1770
- });
1771
-
1772
- // ── priority interaction with principal filtering ──────────────
1773
-
1774
- describe('priority interaction with principal filtering', () => {
1775
- test('higher-priority principal-specific rule wins over lower-priority wildcard', () => {
1776
- seedRules([
1777
- {
1778
- id: 'wildcard-low',
1779
- tool: 'bash',
1780
- pattern: 'test *',
1781
- scope: 'everywhere',
1782
- decision: 'deny',
1783
- priority: 50,
1784
- createdAt: Date.now(),
1785
- },
1786
- {
1787
- id: 'specific-high',
1788
- tool: 'bash',
1789
- pattern: 'test *',
1790
- scope: 'everywhere',
1791
- decision: 'allow',
1792
- priority: 200,
1793
- createdAt: Date.now(),
1794
- principalKind: 'skill',
1795
- principalId: 'tester',
1796
- },
1797
- ]);
1798
- const match = findHighestPriorityRule('bash', ['test unit'], '/tmp', {
1799
- principal: { kind: 'skill', id: 'tester' },
1800
- });
1801
- expect(match).not.toBeNull();
1802
- expect(match!.id).toBe('specific-high');
1803
- expect(match!.decision).toBe('allow');
1804
- });
1805
-
1806
- test('non-matching principal rule is skipped, falling through to wildcard rule', () => {
1807
- seedRules([
1808
- {
1809
- id: 'specific-high',
1810
- tool: 'bash',
1811
- pattern: 'test *',
1812
- scope: 'everywhere',
1813
- decision: 'allow',
1814
- priority: 200,
1815
- createdAt: Date.now(),
1816
- principalKind: 'skill',
1817
- principalId: 'deployer',
1818
- },
1819
- {
1820
- id: 'wildcard-low',
1821
- tool: 'bash',
1822
- pattern: 'test *',
1823
- scope: 'everywhere',
1824
- decision: 'deny',
1825
- priority: 50,
1826
- createdAt: Date.now(),
1827
- },
1828
- ]);
1829
- // Context has kind=skill, id=tester — doesn't match 'deployer'
1830
- const match = findHighestPriorityRule('bash', ['test unit'], '/tmp', {
1831
- principal: { kind: 'skill', id: 'tester' },
1832
- });
1833
- expect(match).not.toBeNull();
1834
- expect(match!.id).toBe('wildcard-low');
1835
- expect(match!.decision).toBe('deny');
1836
- });
1837
- });
1838
-
1839
1427
  // ── backward compatibility ────────────────────────────────────
1840
1428
 
1841
1429
  describe('backward compatibility', () => {
@@ -1847,19 +1435,6 @@ describe('Trust Store', () => {
1847
1435
  expect(match!.pattern).toBe('git *');
1848
1436
  });
1849
1437
 
1850
- test('existing default rules (no principal fields) match with any context', () => {
1851
- // Default rules have no principal fields and should match regardless of context.
1852
- // Use host_file_read which has a default ask rule (default:ask-host_file_read-global).
1853
- const match = findHighestPriorityRule(
1854
- 'host_file_read',
1855
- ['host_file_read:/etc/hosts'],
1856
- '/tmp',
1857
- { principal: { kind: 'skill', id: 'random-skill', version: 'v99' } },
1858
- );
1859
- expect(match).not.toBeNull();
1860
- expect(match!.decision).toBe('ask');
1861
- });
1862
-
1863
1438
  test('empty PolicyContext object behaves the same as no context', () => {
1864
1439
  addRule('bash', 'ls *', '/tmp', 'allow', 200);
1865
1440
  const matchNoCtx = findHighestPriorityRule('bash', ['ls -la'], '/tmp');