@vellumai/assistant 0.4.49 → 0.4.50
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.
- package/ARCHITECTURE.md +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +4 -10
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +31 -3
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +12 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +1 -3
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
getApp,
|
|
12
|
+
getConnection,
|
|
12
13
|
getConnectionByProvider,
|
|
13
14
|
getProvider,
|
|
14
15
|
updateConnection,
|
|
@@ -116,14 +117,14 @@ function recordRefreshFailure(service: string): void {
|
|
|
116
117
|
|
|
117
118
|
const inflightRefreshes = new Map<string, Promise<string>>();
|
|
118
119
|
|
|
119
|
-
function deduplicatedRefresh(service: string): Promise<string> {
|
|
120
|
-
const existing = inflightRefreshes.get(
|
|
120
|
+
function deduplicatedRefresh(service: string, connId: string): Promise<string> {
|
|
121
|
+
const existing = inflightRefreshes.get(connId);
|
|
121
122
|
if (existing) return existing;
|
|
122
123
|
|
|
123
|
-
const promise = doRefresh(service).finally(() => {
|
|
124
|
-
inflightRefreshes.delete(
|
|
124
|
+
const promise = doRefresh(service, connId).finally(() => {
|
|
125
|
+
inflightRefreshes.delete(connId);
|
|
125
126
|
});
|
|
126
|
-
inflightRefreshes.set(
|
|
127
|
+
inflightRefreshes.set(connId, promise);
|
|
127
128
|
return promise;
|
|
128
129
|
}
|
|
129
130
|
|
|
@@ -157,18 +158,11 @@ export class TokenExpiredError extends Error {
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
|
-
* Check whether
|
|
161
|
-
* within the buffer window, based on the `expiresAt` field in the
|
|
162
|
-
* oauth_connection row.
|
|
161
|
+
* Check whether a token is expired or will expire within the buffer window.
|
|
163
162
|
*/
|
|
164
|
-
function isTokenExpired(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (!conn?.expiresAt) return false;
|
|
168
|
-
return Date.now() >= conn.expiresAt - EXPIRY_BUFFER_MS;
|
|
169
|
-
} catch {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
163
|
+
function isTokenExpired(expiresAt: number | null): boolean {
|
|
164
|
+
if (!expiresAt) return false;
|
|
165
|
+
return Date.now() >= expiresAt - EXPIRY_BUFFER_MS;
|
|
172
166
|
}
|
|
173
167
|
|
|
174
168
|
// ── Refresh config resolution ─────────────────────────────────────────
|
|
@@ -191,8 +185,8 @@ interface RefreshConfig {
|
|
|
191
185
|
* authMethod. Throws `TokenExpiredError` if the connection is not found
|
|
192
186
|
* or incomplete.
|
|
193
187
|
*/
|
|
194
|
-
function resolveRefreshConfig(service: string): RefreshConfig {
|
|
195
|
-
const conn =
|
|
188
|
+
function resolveRefreshConfig(service: string, connId: string): RefreshConfig {
|
|
189
|
+
const conn = getConnection(connId);
|
|
196
190
|
if (!conn) {
|
|
197
191
|
throw new TokenExpiredError(
|
|
198
192
|
service,
|
|
@@ -217,15 +211,15 @@ function resolveRefreshConfig(service: string): RefreshConfig {
|
|
|
217
211
|
}
|
|
218
212
|
|
|
219
213
|
const tokenUrl = provider.tokenUrl;
|
|
220
|
-
const
|
|
221
|
-
if (!tokenUrl || !
|
|
214
|
+
const resolvedClientId = app.clientId;
|
|
215
|
+
if (!tokenUrl || !resolvedClientId) {
|
|
222
216
|
throw new TokenExpiredError(
|
|
223
217
|
service,
|
|
224
218
|
`Missing OAuth2 refresh config for "${service}".${recoveryHint(service)}`,
|
|
225
219
|
);
|
|
226
220
|
}
|
|
227
221
|
|
|
228
|
-
const secret = getSecureKey(
|
|
222
|
+
const secret = getSecureKey(app.clientSecretCredentialPath);
|
|
229
223
|
|
|
230
224
|
const refreshToken = getSecureKey(
|
|
231
225
|
`oauth_connection/${conn.id}/refresh_token`,
|
|
@@ -238,7 +232,7 @@ function resolveRefreshConfig(service: string): RefreshConfig {
|
|
|
238
232
|
return {
|
|
239
233
|
connId: conn.id,
|
|
240
234
|
tokenUrl,
|
|
241
|
-
clientId,
|
|
235
|
+
clientId: resolvedClientId,
|
|
242
236
|
secret,
|
|
243
237
|
refreshToken,
|
|
244
238
|
authMethod,
|
|
@@ -254,10 +248,15 @@ function resolveRefreshConfig(service: string): RefreshConfig {
|
|
|
254
248
|
* Returns the new access token on success.
|
|
255
249
|
* Throws `TokenExpiredError` if refresh is not possible.
|
|
256
250
|
*/
|
|
257
|
-
async function doRefresh(service: string): Promise<string> {
|
|
258
|
-
const refreshConfig = resolveRefreshConfig(service);
|
|
259
|
-
const {
|
|
260
|
-
|
|
251
|
+
async function doRefresh(service: string, connId: string): Promise<string> {
|
|
252
|
+
const refreshConfig = resolveRefreshConfig(service, connId);
|
|
253
|
+
const {
|
|
254
|
+
tokenUrl,
|
|
255
|
+
clientId: resolvedClientId,
|
|
256
|
+
secret,
|
|
257
|
+
authMethod,
|
|
258
|
+
refreshToken,
|
|
259
|
+
} = refreshConfig;
|
|
261
260
|
|
|
262
261
|
if (!refreshToken) {
|
|
263
262
|
throw new TokenExpiredError(
|
|
@@ -266,8 +265,8 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
266
265
|
);
|
|
267
266
|
}
|
|
268
267
|
|
|
269
|
-
if (isRefreshBreakerOpen(
|
|
270
|
-
const state = refreshBreakers.get(
|
|
268
|
+
if (isRefreshBreakerOpen(connId)) {
|
|
269
|
+
const state = refreshBreakers.get(connId)!;
|
|
271
270
|
const remainingMs = state.cooldownMs - (Date.now() - state.openedAt);
|
|
272
271
|
throw new TokenExpiredError(
|
|
273
272
|
service,
|
|
@@ -282,13 +281,13 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
282
281
|
try {
|
|
283
282
|
result = await refreshOAuth2Token(
|
|
284
283
|
tokenUrl,
|
|
285
|
-
|
|
284
|
+
resolvedClientId,
|
|
286
285
|
refreshToken,
|
|
287
286
|
secret,
|
|
288
287
|
authMethod,
|
|
289
288
|
);
|
|
290
289
|
} catch (err) {
|
|
291
|
-
recordRefreshFailure(
|
|
290
|
+
recordRefreshFailure(connId);
|
|
292
291
|
if (isCredentialError(err)) {
|
|
293
292
|
const msg = err instanceof Error ? err.message : String(err);
|
|
294
293
|
throw new TokenExpiredError(
|
|
@@ -349,7 +348,7 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
349
348
|
);
|
|
350
349
|
}
|
|
351
350
|
|
|
352
|
-
recordRefreshSuccess(
|
|
351
|
+
recordRefreshSuccess(connId);
|
|
353
352
|
log.info({ service }, "OAuth2 access token refreshed successfully");
|
|
354
353
|
return result.accessToken;
|
|
355
354
|
}
|
|
@@ -368,12 +367,13 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
368
367
|
export async function withValidToken<T>(
|
|
369
368
|
service: string,
|
|
370
369
|
callback: (token: string) => Promise<T>,
|
|
370
|
+
clientId?: string,
|
|
371
371
|
): Promise<T> {
|
|
372
|
-
const conn = getConnectionByProvider(service);
|
|
372
|
+
const conn = getConnectionByProvider(service, clientId);
|
|
373
373
|
let token = conn
|
|
374
374
|
? getSecureKey(`oauth_connection/${conn.id}/access_token`)
|
|
375
375
|
: undefined;
|
|
376
|
-
if (!token) {
|
|
376
|
+
if (!token || !conn) {
|
|
377
377
|
throw new TokenExpiredError(
|
|
378
378
|
service,
|
|
379
379
|
`No access token found for "${service}". Authorization required.${recoveryHint(service)}`,
|
|
@@ -381,15 +381,15 @@ export async function withValidToken<T>(
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
// Proactively refresh if expired or about to expire.
|
|
384
|
-
if (isTokenExpired(
|
|
385
|
-
token = await deduplicatedRefresh(service);
|
|
384
|
+
if (isTokenExpired(conn.expiresAt)) {
|
|
385
|
+
token = await deduplicatedRefresh(service, conn.id);
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
try {
|
|
389
389
|
return await callback(token);
|
|
390
390
|
} catch (err: unknown) {
|
|
391
391
|
if (is401Error(err)) {
|
|
392
|
-
token = await deduplicatedRefresh(service);
|
|
392
|
+
token = await deduplicatedRefresh(service, conn.id);
|
|
393
393
|
return callback(token);
|
|
394
394
|
}
|
|
395
395
|
throw err;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
writeFileSync,
|
|
11
11
|
} from "node:fs";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
|
-
import { dirname, join } from "node:path";
|
|
13
|
+
import { dirname, join, posix, resolve, sep } from "node:path";
|
|
14
14
|
import { gunzipSync } from "node:zlib";
|
|
15
15
|
|
|
16
16
|
import { getLogger } from "../util/logger.js";
|
|
@@ -160,16 +160,35 @@ export function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
|
|
|
160
160
|
|
|
161
161
|
// Skip directories and empty names
|
|
162
162
|
if (name && typeFlag !== 53 /* '5' */) {
|
|
163
|
-
// Prevent path traversal
|
|
164
|
-
const normalizedName = name.replace(
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
// Prevent path traversal and absolute path writes
|
|
164
|
+
const normalizedName = name.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
165
|
+
const normalizedPath = posix.normalize(normalizedName);
|
|
166
|
+
const hasWindowsDrivePrefix = /^[a-zA-Z]:\//.test(normalizedPath);
|
|
167
|
+
const isTraversal =
|
|
168
|
+
normalizedPath === ".." || normalizedPath.startsWith("../");
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
normalizedPath &&
|
|
172
|
+
normalizedPath !== "." &&
|
|
173
|
+
!normalizedPath.startsWith("/") &&
|
|
174
|
+
!hasWindowsDrivePrefix &&
|
|
175
|
+
!isTraversal
|
|
176
|
+
) {
|
|
177
|
+
const destRoot = resolve(destDir);
|
|
178
|
+
const destPath = resolve(destRoot, normalizedPath);
|
|
179
|
+
const insideDestination =
|
|
180
|
+
destPath === destRoot || destPath.startsWith(destRoot + sep);
|
|
181
|
+
if (!insideDestination) {
|
|
182
|
+
offset += Math.ceil(size / 512) * 512;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
167
186
|
mkdirSync(dirname(destPath), { recursive: true });
|
|
168
187
|
writeFileSync(destPath, tarBuffer.subarray(offset, offset + size));
|
|
169
188
|
|
|
170
189
|
if (
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
normalizedPath === "SKILL.md" ||
|
|
191
|
+
normalizedPath.endsWith("/SKILL.md")
|
|
173
192
|
) {
|
|
174
193
|
foundSkillMd = true;
|
|
175
194
|
}
|
|
@@ -319,27 +338,63 @@ export async function installSkillLocally(
|
|
|
319
338
|
|
|
320
339
|
// ─── Auto-install (for skill_load) ──────────────────────────────────────────
|
|
321
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Resolve the catalog skill list, checking local (dev mode) first, then remote.
|
|
343
|
+
*
|
|
344
|
+
* In dev mode with a local catalog, returns local entries immediately to avoid
|
|
345
|
+
* unnecessary network latency. Pass `skillId` to trigger a deferred remote
|
|
346
|
+
* fetch only when the requested skill is not found locally — this preserves the
|
|
347
|
+
* ability to discover remote-only skills without penalising every call with a
|
|
348
|
+
* 10s timeout on flaky networks.
|
|
349
|
+
*
|
|
350
|
+
* Callers that install multiple skills in a loop should call this once and pass
|
|
351
|
+
* the result to `autoInstallFromCatalog` to avoid redundant network requests.
|
|
352
|
+
*/
|
|
353
|
+
export async function resolveCatalog(
|
|
354
|
+
skillId?: string,
|
|
355
|
+
): Promise<CatalogSkill[]> {
|
|
356
|
+
const repoSkillsDir = getRepoSkillsDir();
|
|
357
|
+
if (repoSkillsDir) {
|
|
358
|
+
const local = readLocalCatalog(repoSkillsDir);
|
|
359
|
+
if (local.length > 0) {
|
|
360
|
+
// If no specific skill requested, or it exists locally, skip remote fetch
|
|
361
|
+
if (!skillId || local.some((s) => s.id === skillId)) {
|
|
362
|
+
return local;
|
|
363
|
+
}
|
|
364
|
+
// Skill not found locally — merge with remote so remote-only skills
|
|
365
|
+
// can still be discovered. Local entries take precedence by id.
|
|
366
|
+
try {
|
|
367
|
+
const remote = await fetchCatalog();
|
|
368
|
+
const localIds = new Set(local.map((s) => s.id));
|
|
369
|
+
return [...local, ...remote.filter((s) => !localIds.has(s.id))];
|
|
370
|
+
} catch {
|
|
371
|
+
return local;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return fetchCatalog();
|
|
377
|
+
}
|
|
378
|
+
|
|
322
379
|
/**
|
|
323
380
|
* Attempt to find and install a skill from the first-party catalog.
|
|
324
381
|
* Returns true if the skill was installed, false if not found in catalog.
|
|
325
382
|
* Throws on install failures (network, filesystem, etc).
|
|
383
|
+
*
|
|
384
|
+
* When `catalog` is provided it is used directly, avoiding a redundant
|
|
385
|
+
* network fetch — pass a pre-resolved catalog when calling in a loop.
|
|
326
386
|
*/
|
|
327
387
|
export async function autoInstallFromCatalog(
|
|
328
388
|
skillId: string,
|
|
389
|
+
catalog?: CatalogSkill[],
|
|
329
390
|
): Promise<boolean> {
|
|
330
|
-
|
|
331
|
-
const repoSkillsDir = getRepoSkillsDir();
|
|
332
|
-
let entry: CatalogSkill | undefined;
|
|
391
|
+
let skills: CatalogSkill[];
|
|
333
392
|
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (!entry) {
|
|
393
|
+
if (catalog) {
|
|
394
|
+
skills = catalog;
|
|
395
|
+
} else {
|
|
340
396
|
try {
|
|
341
|
-
|
|
342
|
-
entry = remoteCatalog.find((s) => s.id === skillId);
|
|
397
|
+
skills = await resolveCatalog(skillId);
|
|
343
398
|
} catch (err) {
|
|
344
399
|
log.warn(
|
|
345
400
|
{ err, skillId },
|
|
@@ -349,6 +404,7 @@ export async function autoInstallFromCatalog(
|
|
|
349
404
|
}
|
|
350
405
|
}
|
|
351
406
|
|
|
407
|
+
const entry = skills.find((s) => s.id === skillId);
|
|
352
408
|
if (!entry) {
|
|
353
409
|
return false;
|
|
354
410
|
}
|