vellum 0.2.13 → 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 (207) 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 +113 -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 +137 -18
  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 +62 -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 +27 -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 +4 -4
  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 +93 -7
  92. package/src/calls/call-store.ts +6 -0
  93. package/src/calls/elevenlabs-client.ts +8 -0
  94. package/src/calls/elevenlabs-config.ts +7 -5
  95. package/src/calls/twilio-provider.ts +91 -0
  96. package/src/calls/twilio-routes.ts +32 -37
  97. package/src/calls/types.ts +3 -1
  98. package/src/calls/voice-quality.ts +29 -7
  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 +142 -34
  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 +10 -4
  116. package/src/config/schema.ts +80 -21
  117. package/src/config/types.ts +1 -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/prompter.ts +0 -4
  160. package/src/permissions/shell-identity.ts +227 -0
  161. package/src/permissions/trust-store.ts +76 -53
  162. package/src/permissions/types.ts +0 -19
  163. package/src/permissions/workspace-policy.ts +114 -0
  164. package/src/providers/retry.ts +12 -37
  165. package/src/runtime/assistant-event-hub.ts +41 -4
  166. package/src/runtime/channel-approval-parser.ts +60 -0
  167. package/src/runtime/channel-approval-types.ts +71 -0
  168. package/src/runtime/channel-approvals.ts +145 -0
  169. package/src/runtime/gateway-client.ts +16 -0
  170. package/src/runtime/http-server.ts +29 -9
  171. package/src/runtime/routes/call-routes.ts +52 -2
  172. package/src/runtime/routes/channel-routes.ts +296 -16
  173. package/src/runtime/routes/events-routes.ts +97 -28
  174. package/src/runtime/routes/run-routes.ts +2 -7
  175. package/src/runtime/run-orchestrator.ts +0 -3
  176. package/src/schedule/recurrence-engine.ts +26 -2
  177. package/src/schedule/recurrence-types.ts +1 -1
  178. package/src/schedule/schedule-store.ts +12 -3
  179. package/src/security/secret-scanner.ts +7 -0
  180. package/src/tasks/ephemeral-permissions.ts +0 -2
  181. package/src/tasks/task-scheduler.ts +2 -1
  182. package/src/tools/calls/call-start.ts +8 -0
  183. package/src/tools/execution-target.ts +21 -0
  184. package/src/tools/execution-timeout.ts +49 -0
  185. package/src/tools/executor.ts +6 -135
  186. package/src/tools/network/web-search.ts +9 -32
  187. package/src/tools/policy-context.ts +29 -0
  188. package/src/tools/schedule/update.ts +8 -1
  189. package/src/tools/terminal/parser.ts +16 -18
  190. package/src/tools/types.ts +4 -11
  191. package/src/twitter/oauth-client.ts +102 -0
  192. package/src/twitter/router.ts +101 -0
  193. package/src/util/debounce.ts +88 -0
  194. package/src/util/network-info.ts +47 -0
  195. package/src/util/platform.ts +29 -4
  196. package/src/util/promise-guard.ts +37 -0
  197. package/src/util/retry.ts +98 -0
  198. package/src/util/truncate.ts +1 -1
  199. package/src/workspace/git-service.ts +129 -112
  200. package/src/tools/contacts/contact-merge.ts +0 -55
  201. package/src/tools/contacts/contact-search.ts +0 -58
  202. package/src/tools/contacts/contact-upsert.ts +0 -64
  203. package/src/tools/playbooks/index.ts +0 -4
  204. package/src/tools/playbooks/playbook-create.ts +0 -96
  205. package/src/tools/playbooks/playbook-delete.ts +0 -52
  206. package/src/tools/playbooks/playbook-list.ts +0 -74
  207. 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
  });
@@ -1026,12 +1002,9 @@ describe('Trust Store', () => {
1026
1002
  // ── trust rule schema v3 (PR 14) ──────────────────────────────
1027
1003
 
1028
1004
  describe('trust rule schema v3 (PR 14)', () => {
1029
- test('new rules can include principal fields', () => {
1005
+ test('new rules can include v3 optional fields', () => {
1030
1006
  const rule = addRule('bash', 'git *', '/tmp');
1031
- // Manually set v3 principal fields on the rule and persist
1032
- rule.principalKind = 'skill';
1033
- rule.principalId = 'my-skill';
1034
- rule.principalVersion = 'abc123';
1007
+ // Manually set v3 optional fields on the rule and persist
1035
1008
  rule.executionTarget = '/usr/local/bin/node';
1036
1009
  rule.allowHighRisk = true;
1037
1010
  // Re-persist the updated rules
@@ -1045,9 +1018,6 @@ describe('Trust Store', () => {
1045
1018
  const reloaded = getAllRules();
1046
1019
  const found = reloaded.find((r) => r.id === rule.id);
1047
1020
  expect(found).toBeDefined();
1048
- expect(found!.principalKind).toBe('skill');
1049
- expect(found!.principalId).toBe('my-skill');
1050
- expect(found!.principalVersion).toBe('abc123');
1051
1021
  expect(found!.executionTarget).toBe('/usr/local/bin/node');
1052
1022
  expect(found!.allowHighRisk).toBe(true);
1053
1023
  });
@@ -1072,7 +1042,7 @@ describe('Trust Store', () => {
1072
1042
  expect(data.version).toBe(3);
1073
1043
  });
1074
1044
 
1075
- test('v2 rules survive v3 migration with no principal fields', () => {
1045
+ test('v2 rules survive v3 migration with no v3-only fields', () => {
1076
1046
  mkdirSync(dirname(trustPath), { recursive: true });
1077
1047
  writeFileSync(trustPath, JSON.stringify({
1078
1048
  version: 2,
@@ -1105,10 +1075,7 @@ describe('Trust Store', () => {
1105
1075
  expect(ruleB).toBeDefined();
1106
1076
  expect(ruleA!.pattern).toBe('git *');
1107
1077
  expect(ruleB!.decision).toBe('deny');
1108
- // No principal fields should be present
1109
- expect(ruleA).not.toHaveProperty('principalKind');
1110
- expect(ruleA).not.toHaveProperty('principalId');
1111
- expect(ruleA).not.toHaveProperty('principalVersion');
1078
+ // No v3-only fields should be present
1112
1079
  expect(ruleA).not.toHaveProperty('executionTarget');
1113
1080
  expect(ruleA).not.toHaveProperty('allowHighRisk');
1114
1081
  });
@@ -1254,25 +1221,22 @@ describe('Trust Store', () => {
1254
1221
  expect(userRules).toHaveLength(1);
1255
1222
  });
1256
1223
 
1257
- 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', () => {
1258
1225
  mkdirSync(dirname(trustPath), { recursive: true });
1259
1226
  const v3Rules = [
1260
1227
  {
1261
- id: 'v3-with-principal',
1228
+ id: 'v3-with-options',
1262
1229
  tool: 'bash',
1263
1230
  pattern: 'skill-cmd *',
1264
1231
  scope: '/tmp',
1265
1232
  decision: 'allow',
1266
1233
  priority: 100,
1267
1234
  createdAt: 7000,
1268
- principalKind: 'skill',
1269
- principalId: 'my-skill',
1270
- principalVersion: 'sha256-abc',
1271
1235
  executionTarget: '/usr/bin/node',
1272
1236
  allowHighRisk: false,
1273
1237
  },
1274
1238
  {
1275
- id: 'v3-without-principal',
1239
+ id: 'v3-without-options',
1276
1240
  tool: 'bash',
1277
1241
  pattern: 'git *',
1278
1242
  scope: '/tmp',
@@ -1285,23 +1249,19 @@ describe('Trust Store', () => {
1285
1249
  clearCache();
1286
1250
  const rules = getAllRules();
1287
1251
 
1288
- // Rule with principal fields should have them preserved
1289
- const withPrincipal = rules.find((r) => r.id === 'v3-with-principal');
1290
- expect(withPrincipal).toBeDefined();
1291
- expect(withPrincipal!.principalKind).toBe('skill');
1292
- expect(withPrincipal!.principalId).toBe('my-skill');
1293
- expect(withPrincipal!.principalVersion).toBe('sha256-abc');
1294
- expect(withPrincipal!.executionTarget).toBe('/usr/bin/node');
1295
- 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);
1296
1257
 
1297
- // Rule without principal fields should remain without them
1298
- const withoutPrincipal = rules.find((r) => r.id === 'v3-without-principal');
1299
- expect(withoutPrincipal).toBeDefined();
1300
- expect(withoutPrincipal).not.toHaveProperty('principalKind');
1301
- 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');
1302
1262
  });
1303
1263
 
1304
- test('v2 migration preserves rule meaning exactly — no default principal values added', () => {
1264
+ test('v2 migration preserves rule meaning exactly — no extra fields added', () => {
1305
1265
  mkdirSync(dirname(trustPath), { recursive: true });
1306
1266
  const originalRules = [
1307
1267
  {
@@ -1337,22 +1297,10 @@ describe('Trust Store', () => {
1337
1297
  expect(migrated!.decision).toBe(original.decision);
1338
1298
  expect(migrated!.priority).toBe(original.priority);
1339
1299
  expect(migrated!.createdAt).toBe(original.createdAt);
1340
- // No principal fields were injected by migration
1341
- expect(migrated).not.toHaveProperty('principalKind');
1342
- expect(migrated).not.toHaveProperty('principalId');
1343
- expect(migrated).not.toHaveProperty('principalVersion');
1300
+ // No extra fields were injected by migration
1344
1301
  expect(migrated).not.toHaveProperty('executionTarget');
1345
1302
  expect(migrated).not.toHaveProperty('allowHighRisk');
1346
1303
  }
1347
-
1348
- // Verify disk representation also has no principal fields on user rules
1349
- const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
1350
- for (const original of originalRules) {
1351
- const diskRule = data.rules.find((r: { id: string }) => r.id === original.id);
1352
- expect(diskRule).toBeDefined();
1353
- expect(diskRule).not.toHaveProperty('principalKind');
1354
- expect(diskRule).not.toHaveProperty('principalId');
1355
- }
1356
1304
  });
1357
1305
 
1358
1306
  test('v1 → v3 full migration preserves rules and adds priority', () => {
@@ -1374,54 +1322,15 @@ describe('Trust Store', () => {
1374
1322
  expect(rule).toBeDefined();
1375
1323
  // v1 → v2 adds priority 100
1376
1324
  expect(rule!.priority).toBe(100);
1377
- // v2 → v3 adds no principal fields
1378
- expect(rule).not.toHaveProperty('principalKind');
1379
- expect(rule).not.toHaveProperty('principalId');
1380
1325
  // File should be v3 on disk
1381
1326
  const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
1382
1327
  expect(data.version).toBe(3);
1383
1328
  });
1384
1329
  });
1385
1330
 
1386
- // ── backward compat: addRule without principal options (PR 2/40) ──
1387
- // These tests verify that addRule() without explicit principal options
1388
- // creates wildcard rules. The TrustRule schema *does* support principal
1389
- // and version fields (since PR 14), but they are only set when explicitly
1390
- // provided via the options parameter.
1391
-
1392
- describe('backward compat: addRule without principal options (PR 2/40)', () => {
1393
- test('addRule without principal options creates rules without principal fields', () => {
1394
- const rule = addRule('skill_test_tool', 'skill_test_tool:*', '/tmp');
1395
- expect(rule).not.toHaveProperty('principalKind');
1396
- expect(rule).not.toHaveProperty('principalId');
1397
- expect(rule).not.toHaveProperty('principalVersion');
1398
- expect(rule).not.toHaveProperty('executionTarget');
1399
- expect(rule).not.toHaveProperty('allowHighRisk');
1400
- });
1401
-
1402
- test('findHighestPriorityRule matches without policy context (backward compat)', () => {
1403
- addRule('skill_test_tool', 'skill_test_tool:*', '/tmp', 'allow', 200);
1404
- // Calling without the optional 4th ctx parameter still matches wildcard rules
1405
- const match = findHighestPriorityRule('skill_test_tool', ['skill_test_tool:do-thing'], '/tmp');
1406
- expect(match).not.toBeNull();
1407
- expect(match!.decision).toBe('allow');
1408
- });
1409
-
1410
- test('trust file schema is v3 (rules created without principal fields)', () => {
1411
- addRule('skill_test_tool', 'skill_test_tool:*', '/tmp');
1412
- const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
1413
- expect(raw.version).toBe(3);
1414
- const userRule = raw.rules.find((r: { pattern: string }) => r.pattern === 'skill_test_tool:*');
1415
- expect(userRule).toBeDefined();
1416
- // addRule without principal options doesn't set principal fields
1417
- expect(userRule).not.toHaveProperty('principalVersion');
1418
- expect(userRule).not.toHaveProperty('principalKind');
1419
- });
1420
- });
1421
-
1422
- // ── principal-aware rule matching (PR 16) ──────────────────────
1331
+ // ── executionTarget-aware rule matching ──────────────────────
1423
1332
 
1424
- describe('principal-aware rule matching (PR 16)', () => {
1333
+ describe('executionTarget-aware rule matching', () => {
1425
1334
  /**
1426
1335
  * Helper: write a v3 trust file with the given rules directly to disk,
1427
1336
  * then clear the cache so the next getRules() call picks them up.
@@ -1432,26 +1341,17 @@ describe('Trust Store', () => {
1432
1341
  clearCache();
1433
1342
  }
1434
1343
 
1435
- // ── wildcard semantics (no principal fields on rule) ──────────
1344
+ // ── wildcard semantics (no executionTarget on rule) ──────────
1436
1345
 
1437
- describe('wildcard semantics — rules without principal fields', () => {
1438
- 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', () => {
1439
1348
  addRule('bash', 'git *', '/tmp', 'allow', 200);
1440
1349
  const match = findHighestPriorityRule('bash', ['git status'], '/tmp');
1441
1350
  expect(match).not.toBeNull();
1442
1351
  expect(match!.decision).toBe('allow');
1443
1352
  });
1444
1353
 
1445
- test('rule with no principal fields matches any principal context', () => {
1446
- addRule('bash', 'git *', '/tmp', 'allow', 200);
1447
- const match = findHighestPriorityRule('bash', ['git status'], '/tmp', {
1448
- principal: { kind: 'skill', id: 'my-skill', version: 'v1' },
1449
- });
1450
- expect(match).not.toBeNull();
1451
- expect(match!.decision).toBe('allow');
1452
- });
1453
-
1454
- test('rule with no principal fields matches any execution target', () => {
1354
+ test('rule with no executionTarget matches any execution target', () => {
1455
1355
  addRule('bash', 'git *', '/tmp', 'allow', 200);
1456
1356
  const match = findHighestPriorityRule('bash', ['git status'], '/tmp', {
1457
1357
  executionTarget: '/usr/bin/node',
@@ -1459,186 +1359,6 @@ describe('Trust Store', () => {
1459
1359
  expect(match).not.toBeNull();
1460
1360
  expect(match!.decision).toBe('allow');
1461
1361
  });
1462
-
1463
- test('rule with no principal fields matches context with both principal and target', () => {
1464
- addRule('bash', 'npm *', '/tmp', 'allow', 200);
1465
- const match = findHighestPriorityRule('bash', ['npm install'], '/tmp', {
1466
- principal: { kind: 'skill', id: 'builder', version: 'sha256-xyz' },
1467
- executionTarget: '/usr/local/bin/bun',
1468
- });
1469
- expect(match).not.toBeNull();
1470
- });
1471
- });
1472
-
1473
- // ── principalKind matching ────────────────────────────────────
1474
-
1475
- describe('principalKind matching', () => {
1476
- test('rule with principalKind matches when context kind matches', () => {
1477
- seedRules([{
1478
- id: 'pk-match',
1479
- tool: 'bash',
1480
- pattern: 'echo *',
1481
- scope: 'everywhere',
1482
- decision: 'allow',
1483
- priority: 200,
1484
- createdAt: Date.now(),
1485
- principalKind: 'skill',
1486
- }]);
1487
- const match = findHighestPriorityRule('bash', ['echo hello'], '/tmp', {
1488
- principal: { kind: 'skill' },
1489
- });
1490
- expect(match).not.toBeNull();
1491
- expect(match!.id).toBe('pk-match');
1492
- });
1493
-
1494
- test('rule with principalKind does NOT match when context kind differs', () => {
1495
- seedRules([{
1496
- id: 'pk-mismatch',
1497
- tool: 'bash',
1498
- pattern: 'echo *',
1499
- scope: 'everywhere',
1500
- decision: 'allow',
1501
- priority: 200,
1502
- createdAt: Date.now(),
1503
- principalKind: 'skill',
1504
- }]);
1505
- const match = findHighestPriorityRule('bash', ['echo hello'], '/tmp', {
1506
- principal: { kind: 'core' },
1507
- });
1508
- // Should not match the pk-mismatch rule; may still match a default rule
1509
- expect(match === null || match.id !== 'pk-mismatch').toBe(true);
1510
- });
1511
-
1512
- test('rule with principalKind does NOT match when no context is provided', () => {
1513
- seedRules([{
1514
- id: 'pk-no-ctx',
1515
- tool: 'bash',
1516
- pattern: 'echo *',
1517
- scope: 'everywhere',
1518
- decision: 'allow',
1519
- priority: 200,
1520
- createdAt: Date.now(),
1521
- principalKind: 'skill',
1522
- }]);
1523
- const match = findHighestPriorityRule('bash', ['echo hello'], '/tmp');
1524
- expect(match === null || match.id !== 'pk-no-ctx').toBe(true);
1525
- });
1526
- });
1527
-
1528
- // ── principalId matching ──────────────────────────────────────
1529
-
1530
- describe('principalId matching', () => {
1531
- test('rule with principalKind + principalId matches exact principal', () => {
1532
- seedRules([{
1533
- id: 'pid-exact',
1534
- tool: 'bash',
1535
- pattern: 'deploy *',
1536
- scope: 'everywhere',
1537
- decision: 'allow',
1538
- priority: 200,
1539
- createdAt: Date.now(),
1540
- principalKind: 'skill',
1541
- principalId: 'deployer',
1542
- }]);
1543
- const match = findHighestPriorityRule('bash', ['deploy prod'], '/tmp', {
1544
- principal: { kind: 'skill', id: 'deployer' },
1545
- });
1546
- expect(match).not.toBeNull();
1547
- expect(match!.id).toBe('pid-exact');
1548
- });
1549
-
1550
- test('rule with principalId does NOT match different id', () => {
1551
- seedRules([{
1552
- id: 'pid-diff',
1553
- tool: 'bash',
1554
- pattern: 'deploy *',
1555
- scope: 'everywhere',
1556
- decision: 'allow',
1557
- priority: 200,
1558
- createdAt: Date.now(),
1559
- principalKind: 'skill',
1560
- principalId: 'deployer',
1561
- }]);
1562
- const match = findHighestPriorityRule('bash', ['deploy prod'], '/tmp', {
1563
- principal: { kind: 'skill', id: 'other-skill' },
1564
- });
1565
- expect(match === null || match.id !== 'pid-diff').toBe(true);
1566
- });
1567
- });
1568
-
1569
- // ── principalVersion matching ─────────────────────────────────
1570
-
1571
- describe('principalVersion matching', () => {
1572
- test('rule with principalVersion matches exact version', () => {
1573
- seedRules([{
1574
- id: 'pv-exact',
1575
- tool: 'bash',
1576
- pattern: 'build *',
1577
- scope: 'everywhere',
1578
- decision: 'allow',
1579
- priority: 200,
1580
- createdAt: Date.now(),
1581
- principalKind: 'skill',
1582
- principalId: 'builder',
1583
- principalVersion: 'sha256-abc123',
1584
- }]);
1585
- const match = findHighestPriorityRule('bash', ['build all'], '/tmp', {
1586
- principal: { kind: 'skill', id: 'builder', version: 'sha256-abc123' },
1587
- });
1588
- expect(match).not.toBeNull();
1589
- expect(match!.id).toBe('pv-exact');
1590
- });
1591
-
1592
- test('rule with principalVersion does NOT match different version', () => {
1593
- seedRules([{
1594
- id: 'pv-diff',
1595
- tool: 'bash',
1596
- pattern: 'build *',
1597
- scope: 'everywhere',
1598
- decision: 'allow',
1599
- priority: 200,
1600
- createdAt: Date.now(),
1601
- principalKind: 'skill',
1602
- principalId: 'builder',
1603
- principalVersion: 'sha256-abc123',
1604
- }]);
1605
- const match = findHighestPriorityRule('bash', ['build all'], '/tmp', {
1606
- principal: { kind: 'skill', id: 'builder', version: 'sha256-DIFFERENT' },
1607
- });
1608
- expect(match === null || match.id !== 'pv-diff').toBe(true);
1609
- });
1610
-
1611
- test('rule WITHOUT principalVersion matches any version (wildcard)', () => {
1612
- seedRules([{
1613
- id: 'pv-wildcard',
1614
- tool: 'bash',
1615
- pattern: 'build *',
1616
- scope: 'everywhere',
1617
- decision: 'allow',
1618
- priority: 200,
1619
- createdAt: Date.now(),
1620
- principalKind: 'skill',
1621
- principalId: 'builder',
1622
- // no principalVersion — should match any version
1623
- }]);
1624
- const matchV1 = findHighestPriorityRule('bash', ['build all'], '/tmp', {
1625
- principal: { kind: 'skill', id: 'builder', version: 'v1' },
1626
- });
1627
- expect(matchV1).not.toBeNull();
1628
- expect(matchV1!.id).toBe('pv-wildcard');
1629
-
1630
- const matchV2 = findHighestPriorityRule('bash', ['build all'], '/tmp', {
1631
- principal: { kind: 'skill', id: 'builder', version: 'v2' },
1632
- });
1633
- expect(matchV2).not.toBeNull();
1634
- expect(matchV2!.id).toBe('pv-wildcard');
1635
-
1636
- const matchNoVersion = findHighestPriorityRule('bash', ['build all'], '/tmp', {
1637
- principal: { kind: 'skill', id: 'builder' },
1638
- });
1639
- expect(matchNoVersion).not.toBeNull();
1640
- expect(matchNoVersion!.id).toBe('pv-wildcard');
1641
- });
1642
1362
  });
1643
1363
 
1644
1364
  // ── executionTarget matching ──────────────────────────────────
@@ -1704,139 +1424,6 @@ describe('Trust Store', () => {
1704
1424
  });
1705
1425
  });
1706
1426
 
1707
- // ── combined principal + executionTarget ───────────────────────
1708
-
1709
- describe('combined principal + executionTarget matching', () => {
1710
- test('rule with both principal and executionTarget matches when all fields match', () => {
1711
- seedRules([{
1712
- id: 'combo-match',
1713
- tool: 'bash',
1714
- pattern: 'deploy *',
1715
- scope: 'everywhere',
1716
- decision: 'allow',
1717
- priority: 200,
1718
- createdAt: Date.now(),
1719
- principalKind: 'skill',
1720
- principalId: 'deployer',
1721
- principalVersion: 'sha256-abc',
1722
- executionTarget: '/usr/bin/node',
1723
- }]);
1724
- const match = findHighestPriorityRule('bash', ['deploy prod'], '/tmp', {
1725
- principal: { kind: 'skill', id: 'deployer', version: 'sha256-abc' },
1726
- executionTarget: '/usr/bin/node',
1727
- });
1728
- expect(match).not.toBeNull();
1729
- expect(match!.id).toBe('combo-match');
1730
- });
1731
-
1732
- test('rule with both principal and executionTarget fails if principal mismatches', () => {
1733
- seedRules([{
1734
- id: 'combo-bad-principal',
1735
- tool: 'bash',
1736
- pattern: 'deploy *',
1737
- scope: 'everywhere',
1738
- decision: 'allow',
1739
- priority: 200,
1740
- createdAt: Date.now(),
1741
- principalKind: 'skill',
1742
- principalId: 'deployer',
1743
- executionTarget: '/usr/bin/node',
1744
- }]);
1745
- const match = findHighestPriorityRule('bash', ['deploy prod'], '/tmp', {
1746
- principal: { kind: 'skill', id: 'other-skill' },
1747
- executionTarget: '/usr/bin/node',
1748
- });
1749
- expect(match === null || match.id !== 'combo-bad-principal').toBe(true);
1750
- });
1751
-
1752
- test('rule with both principal and executionTarget fails if target mismatches', () => {
1753
- seedRules([{
1754
- id: 'combo-bad-target',
1755
- tool: 'bash',
1756
- pattern: 'deploy *',
1757
- scope: 'everywhere',
1758
- decision: 'allow',
1759
- priority: 200,
1760
- createdAt: Date.now(),
1761
- principalKind: 'skill',
1762
- principalId: 'deployer',
1763
- executionTarget: '/usr/bin/node',
1764
- }]);
1765
- const match = findHighestPriorityRule('bash', ['deploy prod'], '/tmp', {
1766
- principal: { kind: 'skill', id: 'deployer' },
1767
- executionTarget: '/usr/bin/bun',
1768
- });
1769
- expect(match === null || match.id !== 'combo-bad-target').toBe(true);
1770
- });
1771
- });
1772
-
1773
- // ── priority interaction with principal filtering ──────────────
1774
-
1775
- describe('priority interaction with principal filtering', () => {
1776
- test('higher-priority principal-specific rule wins over lower-priority wildcard', () => {
1777
- seedRules([
1778
- {
1779
- id: 'wildcard-low',
1780
- tool: 'bash',
1781
- pattern: 'test *',
1782
- scope: 'everywhere',
1783
- decision: 'deny',
1784
- priority: 50,
1785
- createdAt: Date.now(),
1786
- },
1787
- {
1788
- id: 'specific-high',
1789
- tool: 'bash',
1790
- pattern: 'test *',
1791
- scope: 'everywhere',
1792
- decision: 'allow',
1793
- priority: 200,
1794
- createdAt: Date.now(),
1795
- principalKind: 'skill',
1796
- principalId: 'tester',
1797
- },
1798
- ]);
1799
- const match = findHighestPriorityRule('bash', ['test unit'], '/tmp', {
1800
- principal: { kind: 'skill', id: 'tester' },
1801
- });
1802
- expect(match).not.toBeNull();
1803
- expect(match!.id).toBe('specific-high');
1804
- expect(match!.decision).toBe('allow');
1805
- });
1806
-
1807
- test('non-matching principal rule is skipped, falling through to wildcard rule', () => {
1808
- seedRules([
1809
- {
1810
- id: 'specific-high',
1811
- tool: 'bash',
1812
- pattern: 'test *',
1813
- scope: 'everywhere',
1814
- decision: 'allow',
1815
- priority: 200,
1816
- createdAt: Date.now(),
1817
- principalKind: 'skill',
1818
- principalId: 'deployer',
1819
- },
1820
- {
1821
- id: 'wildcard-low',
1822
- tool: 'bash',
1823
- pattern: 'test *',
1824
- scope: 'everywhere',
1825
- decision: 'deny',
1826
- priority: 50,
1827
- createdAt: Date.now(),
1828
- },
1829
- ]);
1830
- // Context has kind=skill, id=tester — doesn't match 'deployer'
1831
- const match = findHighestPriorityRule('bash', ['test unit'], '/tmp', {
1832
- principal: { kind: 'skill', id: 'tester' },
1833
- });
1834
- expect(match).not.toBeNull();
1835
- expect(match!.id).toBe('wildcard-low');
1836
- expect(match!.decision).toBe('deny');
1837
- });
1838
- });
1839
-
1840
1427
  // ── backward compatibility ────────────────────────────────────
1841
1428
 
1842
1429
  describe('backward compatibility', () => {
@@ -1848,19 +1435,6 @@ describe('Trust Store', () => {
1848
1435
  expect(match!.pattern).toBe('git *');
1849
1436
  });
1850
1437
 
1851
- test('existing default rules (no principal fields) match with any context', () => {
1852
- // Default rules have no principal fields and should match regardless of context.
1853
- // Use host_file_read which has a default ask rule (default:ask-host_file_read-global).
1854
- const match = findHighestPriorityRule(
1855
- 'host_file_read',
1856
- ['host_file_read:/etc/hosts'],
1857
- '/tmp',
1858
- { principal: { kind: 'skill', id: 'random-skill', version: 'v99' } },
1859
- );
1860
- expect(match).not.toBeNull();
1861
- expect(match!.decision).toBe('ask');
1862
- });
1863
-
1864
1438
  test('empty PolicyContext object behaves the same as no context', () => {
1865
1439
  addRule('bash', 'ls *', '/tmp', 'allow', 200);
1866
1440
  const matchNoCtx = findHighestPriorityRule('bash', ['ls -la'], '/tmp');