@danypops/pi-packed 0.21.14 → 0.22.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.
Files changed (41) hide show
  1. package/dist/client.d.ts +2 -2
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +1 -1
  4. package/dist/protocol.d.ts +33 -0
  5. package/dist/protocol.d.ts.map +1 -1
  6. package/extension/src/approval/reload.ts +51 -2
  7. package/extension/src/menu-theme.ts +13 -3
  8. package/extension/src/packed.ts +2 -2
  9. package/extension/src/tabs/discover.ts +13 -2
  10. package/extension/src/tool-output.ts +14 -1
  11. package/extension/src/tools.ts +46 -14
  12. package/extension/src/tui.ts +7 -3
  13. package/package.json +4 -4
  14. package/service/src/adoption/doctor.ts +15 -0
  15. package/service/src/adoption/install-validation.ts +97 -5
  16. package/service/src/adoption/module-freshness.ts +100 -0
  17. package/service/src/adoption/smoke.ts +26 -2
  18. package/service/src/cli/cli.ts +30 -5
  19. package/service/src/daemon/client.ts +13 -7
  20. package/service/src/daemon/daemon.ts +35 -3
  21. package/service/src/daemon/service.ts +42 -7
  22. package/service/src/daemon/vehicle-registration.ts +118 -35
  23. package/service/src/daemon/watcher.ts +1 -18
  24. package/service/src/packages/deploy-verify.ts +151 -0
  25. package/service/src/packages/install.ts +120 -3
  26. package/service/src/packages/package.ts +67 -1
  27. package/service/src/packages/resources.ts +63 -0
  28. package/service/src/public/client.ts +9 -3
  29. package/service/src/public/protocol.ts +13 -1
  30. package/service/test/cli.test.ts +75 -1
  31. package/service/test/deploy-verify.test.ts +168 -0
  32. package/service/test/doctor.test.ts +104 -1
  33. package/service/test/fixtures/install-validation/convention-only-package/extensions/index.ts +3 -0
  34. package/service/test/fixtures/install-validation/convention-only-package/package.json +5 -0
  35. package/service/test/install-validation.test.ts +14 -1
  36. package/service/test/install.test.ts +229 -0
  37. package/service/test/module-freshness.test.ts +146 -0
  38. package/service/test/resources.test.ts +66 -1
  39. package/service/test/service.test.ts +68 -1
  40. package/service/test/smoke.test.ts +49 -2
  41. package/service/test/vehicle-registration.test.ts +97 -5
@@ -70,14 +70,29 @@ async function invoke(app: { fetch(request: Request): Promise<Response> }, name:
70
70
  }
71
71
 
72
72
  describe("packed's daemon operation surface, through the real Vehicle wire protocol", () => {
73
- it("GET /vehicle/manifest lists all 29 operations, authenticated the same as /api/v1/ops", async () => {
73
+ it("GET /vehicle/manifest lists all 29 operations plus the registry's own vehicle.approval.resolve, authenticated the same as /api/v1/ops", async () => {
74
74
  const app = createApp(deps());
75
75
  const unauthorized = await app.fetch(new Request("http://packed.internal/vehicle/manifest"));
76
76
  expect(unauthorized.status).toBe(401);
77
77
  const response = await app.fetch(new Request("http://packed.internal/vehicle/manifest", { headers: { authorization: "Bearer test-token" } }));
78
78
  expect(response.status).toBe(200);
79
79
  const body = (await response.json()) as { operations: Array<{ name: string }> };
80
- expect(body.operations.map((o) => o.name).sort()).toEqual([...OPERATION_NAMES].sort());
80
+ expect(body.operations.map((o) => o.name).sort()).toEqual([...OPERATION_NAMES, "vehicle.approval.resolve"].sort());
81
+ });
82
+
83
+ it("a mutation's approvalRequired reflects security.ts's own per-operation classification, not a coarse effect default -- restart_service is gated despite sharing external-write with the never-gated catalog.sync", async () => {
84
+ const app = createApp(deps());
85
+ const response = await app.fetch(new Request("http://packed.internal/vehicle/manifest", { headers: { authorization: "Bearer test-token" } }));
86
+ const body = (await response.json()) as { operations: Array<{ name: string; approvalRequired?: boolean }> };
87
+ const byName = new Map(body.operations.map((o) => [o.name, o.approvalRequired]));
88
+ expect(byName.get("package.restart_service")).toBe(true);
89
+ expect(byName.get("package.reconcile_services")).toBe(true);
90
+ expect(byName.get("package.catalog.sync")).toBe(false);
91
+ expect(byName.get("package.index.build")).toBe(false);
92
+ expect(byName.get("resources.toggle")).toBe(true);
93
+ expect(byName.get("package.security.set")).toBe(true);
94
+ expect(byName.get("setup.export")).toBe(false);
95
+ expect(byName.get("setup.update")).toBe(false);
81
96
  });
82
97
 
83
98
  it("package.search round-trips through /vehicle/invoke, matching /api/v1/ops's own shape", async () => {
@@ -102,11 +117,88 @@ describe("packed's daemon operation surface, through the real Vehicle wire proto
102
117
  expect(result.body.error?.category).toBe("authorization");
103
118
  });
104
119
 
105
- it("an approved mutation succeeds through /vehicle/invoke exactly like an approved /api/v1/ops call", async () => {
120
+ it("a genuinely approved mutation (through Vehicle's own request/resolve/capability dance) succeeds exactly like an approved /api/v1/ops call -- an approved:true field in the input body alone is no longer enough, since Vehicle's own gate never reads it", async () => {
106
121
  const app = createApp(deps());
107
- const result = await invoke(app, "package.remove", { name: "pi-lsp", approved: true });
122
+ const stillDenied = await invoke(app, "package.remove", { name: "pi-lsp", approved: true });
123
+ expect(stillDenied.status).toBe(403);
124
+
125
+ const denied = await invoke(app, "package.remove", { name: "pi-lsp" });
126
+ const requestId = (denied.body.error as unknown as { details?: { requestId?: string } }).details?.requestId;
127
+ expect(typeof requestId).toBe("string");
128
+ const resolveResponse = await app.fetch(
129
+ new Request("http://packed.internal/vehicle/invoke", {
130
+ method: "POST",
131
+ headers: { authorization: "Bearer test-token", "content-type": "application/json" },
132
+ body: JSON.stringify({
133
+ name: "vehicle.approval.resolve",
134
+ version: 1,
135
+ input: { requestId, decision: "granted" },
136
+ permissions: ["vehicle:approvals:resolve"],
137
+ }),
138
+ }),
139
+ );
140
+ const resolved = {
141
+ status: resolveResponse.status,
142
+ body: (await resolveResponse.json()) as { output?: { capability?: string } },
143
+ };
144
+ expect(resolved.status).toBe(200);
145
+ const capability = (resolved.body.output as { capability?: string }).capability;
146
+ expect(typeof capability).toBe("string");
147
+
148
+ const response = await app.fetch(
149
+ new Request("http://packed.internal/vehicle/invoke", {
150
+ method: "POST",
151
+ headers: { authorization: "Bearer test-token", "content-type": "application/json" },
152
+ body: JSON.stringify({
153
+ name: "package.remove",
154
+ version: 1,
155
+ input: { name: "pi-lsp" },
156
+ permissions: ["packed:read", "packed:write"],
157
+ approvalCapability: capability,
158
+ }),
159
+ }),
160
+ );
161
+ expect(response.status).toBe(200);
162
+ const body = (await response.json()) as { output: { ok: boolean } };
163
+ expect(body.output.ok).toBe(true);
164
+ });
165
+
166
+ it("mutationApproval: never disables the registry's own gate too, live, the instant /security POST changes it -- no daemon restart", async () => {
167
+ const app = createApp(deps());
168
+ const deniedFirst = await invoke(app, "package.remove", { name: "pi-lsp" });
169
+ expect(deniedFirst.status).toBe(403);
170
+
171
+ const securityPost = await app.fetch(
172
+ new Request("http://packed.internal/security", {
173
+ method: "POST",
174
+ headers: { authorization: "Bearer test-token", "content-type": "application/json" },
175
+ body: JSON.stringify({ mutationApproval: "never", approved: true }),
176
+ }),
177
+ );
178
+ expect(securityPost.status).toBe(200);
179
+
180
+ const allowedNow = await invoke(app, "package.remove", { name: "pi-lsp" });
181
+ expect(allowedNow.status).toBe(200);
182
+ expect((allowedNow.body.output as { ok: boolean }).ok).toBe(true);
183
+ });
184
+
185
+ it("a daemon that boots with mutationApproval already never on disk starts with the gate already disabled, not just after the first /security POST", async () => {
186
+ const stateDir = temporaryRoot("packed-vehicle-never-");
187
+ const bootstrap = createApp(deps({ stateDir }));
188
+ const prep = await bootstrap.fetch(
189
+ new Request("http://packed.internal/security", {
190
+ method: "POST",
191
+ headers: { authorization: "Bearer test-token", "content-type": "application/json" },
192
+ body: JSON.stringify({ mutationApproval: "never", approved: true }),
193
+ }),
194
+ );
195
+ expect(prep.status).toBe(200);
196
+
197
+ // A brand new app instance against the SAME on-disk state -- simulates a fresh daemon
198
+ // process starting up after the setting was already changed by a previous run.
199
+ const freshBoot = createApp(deps({ stateDir }));
200
+ const result = await invoke(freshBoot, "package.remove", { name: "pi-lsp" });
108
201
  expect(result.status).toBe(200);
109
- expect((result.body.output as { ok: boolean }).ok).toBe(true);
110
202
  });
111
203
 
112
204
  it("a handler-thrown validation error keeps its own real message, mapped to Vehicle's validation category (not a generic internal 500)", async () => {