runwork 0.10.4 → 0.12.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 (35) hide show
  1. package/dist/agents/__tests__/claude-code-managed-block.test.d.ts +1 -0
  2. package/dist/agents/__tests__/claude-code-managed-block.test.js +97 -0
  3. package/dist/agents/__tests__/codex-minimum-permissions.test.d.ts +1 -0
  4. package/dist/agents/__tests__/codex-minimum-permissions.test.js +82 -0
  5. package/dist/agents/__tests__/cursor-merge.test.d.ts +1 -0
  6. package/dist/agents/__tests__/cursor-merge.test.js +66 -0
  7. package/dist/agents/__tests__/defaults-merge.test.d.ts +1 -0
  8. package/dist/agents/__tests__/defaults-merge.test.js +268 -0
  9. package/dist/agents/__tests__/intro-skill.test.js +32 -0
  10. package/dist/agents/claude-code.d.ts +5 -0
  11. package/dist/agents/claude-code.js +29 -0
  12. package/dist/agents/cursor.d.ts +23 -1
  13. package/dist/agents/cursor.js +42 -3
  14. package/dist/agents/default-config.d.ts +30 -0
  15. package/dist/agents/default-config.js +67 -0
  16. package/dist/agents/defaults-merge.d.ts +72 -0
  17. package/dist/agents/defaults-merge.js +131 -0
  18. package/dist/agents/intro-skill.d.ts +9 -0
  19. package/dist/agents/intro-skill.js +41 -0
  20. package/dist/agents/types.d.ts +31 -2
  21. package/dist/commands/__tests__/setup-persona.test.d.ts +1 -0
  22. package/dist/commands/__tests__/setup-persona.test.js +31 -0
  23. package/dist/commands/info.d.ts +1 -1
  24. package/dist/commands/info.js +7 -2
  25. package/dist/commands/setup.d.ts +7 -0
  26. package/dist/commands/setup.js +22 -0
  27. package/dist/commands/sync.js +147 -59
  28. package/dist/generated/bundled-types.js +33 -33
  29. package/dist/generated/version.d.ts +1 -1
  30. package/dist/generated/version.js +1 -1
  31. package/dist/types.d.ts +36 -0
  32. package/dist/ui/banner.js +1 -1
  33. package/dist/utils/app-info.d.ts +4 -0
  34. package/dist/utils/app-info.js +17 -0
  35. package/package.json +1 -1
@@ -7,6 +7,8 @@ import { ApiClient } from '../api/client.js';
7
7
  import { getAdapterBySlug, detectAgents } from '../agents/detect.js';
8
8
  import { CodexAdapter } from '../agents/codex.js';
9
9
  import { RUNWORK_MCP_PREFIX, RUNWORK_WORKSPACE_MCP_NAME } from '../agents/types.js';
10
+ import { RUNWORK_AGENT_DEFAULTS, AGENT_DEFAULTS_SCHEMA_VERSION } from '../agents/default-config.js';
11
+ import { resolveAgentDefaults } from '../agents/defaults-merge.js';
10
12
  import { collectTelemetryEvents, printTelemetryVerbose, summarizeTelemetryForDryRun, } from './sync-telemetry.js';
11
13
  import { generateIntroSkill, generateInstructionHint, buildAppSkillDescription } from '../agents/intro-skill.js';
12
14
  import { computeSyncPlan } from '../sync/change-detect.js';
@@ -291,6 +293,7 @@ export async function syncFromState(state, statePath, credentials, opts) {
291
293
  appCount: allSkills.filter(s => s.type === 'app').length,
292
294
  skillCount: remoteSkills.length,
293
295
  mcpServerCount: mcpEntries.length,
296
+ persona: state.persona,
294
297
  });
295
298
  // For project scope: determine which app skill to include (only current app's skill)
296
299
  let projectAppSkillFilter = null;
@@ -346,85 +349,170 @@ export async function syncFromState(state, statePath, credentials, opts) {
346
349
  }
347
350
  }
348
351
  }
349
- // Pull and apply team instructions from onboarding config (non-fatal)
352
+ // Pull team config from server. A failure here must not stop the
353
+ // unified user-scope write below, which applies network and minimum-permission
354
+ // floors regardless of whether team config was fetched.
350
355
  let teamInstructionsApplied = false;
351
356
  let agentConfigsApplied = 0;
357
+ let teamInstructions;
358
+ let agentConfigs;
352
359
  try {
353
360
  const onboardingConfig = await client.getOnboardingConfig(state.workspaceId);
354
- const teamInstructions = onboardingConfig?.config?.teamInstructions;
355
- const agentConfigs = onboardingConfig?.config?.agentConfigs;
356
- // Write team instructions and/or per-agent extra instructions.
357
- // Either or both may be present independently.
361
+ teamInstructions = onboardingConfig?.config?.teamInstructions ?? undefined;
362
+ agentConfigs = onboardingConfig?.config?.agentConfigs ?? undefined;
363
+ }
364
+ catch {
365
+ // Team config pull is non-fatal — proceed with no team-managed overrides.
366
+ }
367
+ // Write team instructions and/or per-agent extra instructions.
368
+ for (const adapter of adapters) {
369
+ if (!adapter.writeTeamInstructions)
370
+ continue;
371
+ const agentExtra = agentConfigs?.[adapter.slug]?.extraInstructions;
372
+ const parts = [teamInstructions, agentExtra].filter(Boolean);
373
+ if (parts.length === 0)
374
+ continue;
375
+ const fullInstructions = parts.join('\n\n');
376
+ for (const scope of scopes) {
377
+ try {
378
+ await adapter.writeTeamInstructions(fullInstructions, scope);
379
+ teamInstructionsApplied = true;
380
+ console.log(` [${adapter.name}] Updated team instructions (${scope})`);
381
+ }
382
+ catch {
383
+ // Best-effort per adapter/scope
384
+ }
385
+ }
386
+ }
387
+ // Apply per-agent project-scope config (team overrides only).
388
+ // User scope is handled below in the unified defaults+network+minimum block.
389
+ if (agentConfigs) {
358
390
  for (const adapter of adapters) {
359
- if (!adapter.writeTeamInstructions)
391
+ const agentConfig = agentConfigs[adapter.slug];
392
+ if (!agentConfig || !adapter.writeAgentConfig)
360
393
  continue;
361
- const agentExtra = agentConfigs?.[adapter.slug]?.extraInstructions;
362
- const parts = [teamInstructions, agentExtra].filter(Boolean);
363
- if (parts.length === 0)
394
+ const { extraInstructions: _, ...configWithoutInstructions } = agentConfig;
395
+ if (Object.keys(configWithoutInstructions).length === 0)
364
396
  continue;
365
- const fullInstructions = parts.join('\n\n');
366
397
  for (const scope of scopes) {
398
+ if (scope === 'user')
399
+ continue; // handled in unified block below
367
400
  try {
368
- await adapter.writeTeamInstructions(fullInstructions, scope);
369
- teamInstructionsApplied = true;
370
- console.log(` [${adapter.name}] Updated team instructions (${scope})`);
401
+ await adapter.writeAgentConfig(configWithoutInstructions, scope);
402
+ agentConfigsApplied++;
403
+ const configKeys = Object.keys(configWithoutInstructions).join(', ');
404
+ console.log(` [${adapter.name}] Updated agent config: ${configKeys} (${scope})`);
371
405
  }
372
406
  catch {
373
407
  // Best-effort per adapter/scope
374
408
  }
375
409
  }
376
410
  }
377
- // Apply per-agent config overrides (model preferences, permissions)
378
- if (agentConfigs) {
379
- for (const adapter of adapters) {
380
- const agentConfig = agentConfigs[adapter.slug];
381
- if (!agentConfig || !adapter.writeAgentConfig)
382
- continue;
383
- // Only apply model/permission config (extraInstructions already handled above)
384
- const { extraInstructions: _, ...configWithoutInstructions } = agentConfig;
385
- if (Object.keys(configWithoutInstructions).length === 0)
386
- continue;
387
- for (const scope of scopes) {
388
- try {
389
- await adapter.writeAgentConfig(configWithoutInstructions, scope);
390
- agentConfigsApplied++;
391
- const configKeys = Object.keys(configWithoutInstructions).join(', ');
392
- console.log(` [${adapter.name}] Updated agent config: ${configKeys} (${scope})`);
411
+ }
412
+ // Unified user-scope write: merge baked defaults with team config, network
413
+ // allowlist, and minimum permissions into a single writeAgentConfig call
414
+ // per adapter. The merge layer tracks injected defaults in state.agentDefaults
415
+ // so user-removed entries become sticky opt-outs.
416
+ if (scopes.includes('user')) {
417
+ const networkDomains = ['runwork.ai', '*.runwork.ai'];
418
+ try {
419
+ const baseHost = new URL(baseUrl).hostname;
420
+ if (baseHost !== 'runwork.ai' && !baseHost.endsWith('.runwork.ai')) {
421
+ networkDomains.push(baseHost, `*.${baseHost}`);
422
+ }
423
+ }
424
+ catch { /* use defaults */ }
425
+ // Order arrays list each agent's valid values from most restrictive to most
426
+ // permissive. The floor logic upgrades anything strictly below `minimum`,
427
+ // including unknown values (-1 < minimumIdx). That makes it critical to
428
+ // include the agent's most-permissive option in `order` — otherwise a user
429
+ // who explicitly set, say, `sandbox_mode = "danger-full-access"` gets
430
+ // silently downgraded to `workspace-write`.
431
+ const minimumPermissions = [
432
+ { field: 'approval_policy', order: ['untrusted', 'on-request', 'on-failure', 'never'], minimum: 'on-request' },
433
+ { field: 'sandbox_mode', order: ['read-only', 'workspace-write', 'danger-full-access'], minimum: 'workspace-write' },
434
+ ];
435
+ // agentDefaults lifecycle: full `runwork uninstall` deletes ~/.runwork
436
+ // (state and all per-agent defaults state with it). Re-running `runwork
437
+ // setup` overwrites setup.json without agentDefaults so every remaining
438
+ // tool bootstraps fresh on the next sync. refreshConfiguredAgents only
439
+ // adds slugs (never removes), so we don't need orphan-entry pruning here.
440
+ if (!state.agentDefaults)
441
+ state.agentDefaults = {};
442
+ for (const adapter of adapters) {
443
+ if (!adapter.writeAgentConfig)
444
+ continue;
445
+ const slug = adapter.slug;
446
+ const baked = RUNWORK_AGENT_DEFAULTS[slug];
447
+ const agentState = state.agentDefaults[slug];
448
+ const team = agentConfigs?.[slug];
449
+ // onDisk = undefined signals "removal detection is not possible" — used for
450
+ // markerless adapters (Cursor) and for any adapter without readManagedBlock.
451
+ // The resolver treats undefined differently from an empty array: it skips
452
+ // opt-out fabrication entirely and trusts the baseline mechanism in the
453
+ // adapter to preserve user edits via subtraction.
454
+ let onDisk;
455
+ if (baked && adapter.readManagedBlock) {
456
+ try {
457
+ onDisk = await adapter.readManagedBlock('user');
458
+ }
459
+ catch {
460
+ onDisk = undefined;
461
+ }
462
+ }
463
+ const resolved = resolveAgentDefaults({
464
+ baked,
465
+ state: agentState,
466
+ onDisk,
467
+ team: team?.permissionRules,
468
+ });
469
+ const mergedConfig = {
470
+ modelPreference: team?.modelPreference,
471
+ permissionRules: (resolved.managedAllow.length || resolved.managedDeny.length || team?.permissionRules?.defaultMode)
472
+ ? {
473
+ ...(resolved.managedAllow.length ? { allow: resolved.managedAllow } : {}),
474
+ ...(resolved.managedDeny.length ? { deny: resolved.managedDeny } : {}),
475
+ ...(team?.permissionRules?.defaultMode ? { defaultMode: team.permissionRules.defaultMode } : {}),
393
476
  }
394
- catch {
395
- // Best-effort per adapter/scope
477
+ : undefined,
478
+ networkAllowlist: networkDomains,
479
+ minimumPermissions,
480
+ };
481
+ try {
482
+ const baseline = agentState?.lastInjected;
483
+ await adapter.writeAgentConfig(mergedConfig, 'user', baseline);
484
+ if (baked) {
485
+ const nextState = {
486
+ lastInjected: {
487
+ allow: resolved.applicableAllow,
488
+ deny: resolved.applicableDeny,
489
+ },
490
+ userOptOuts: {
491
+ allow: resolved.newOptOutsAllow,
492
+ deny: resolved.newOptOutsDeny,
493
+ },
494
+ };
495
+ state.agentDefaults[slug] = nextState;
496
+ // Diagnostic log line (per "invisible / sync log only" policy)
497
+ if (resolved.bootstrapped) {
498
+ console.log(` [${adapter.name}] Bootstrapped default-rule tracking (defaults apply on next sync)`);
499
+ }
500
+ else if (resolved.applicableAllow.length || resolved.applicableDeny.length || resolved.removalsThisSync.allow || resolved.removalsThisSync.deny) {
501
+ const totalRemovals = resolved.removalsThisSync.allow + resolved.removalsThisSync.deny;
502
+ const removalNote = totalRemovals > 0
503
+ ? ` (${totalRemovals} new opt-out${totalRemovals > 1 ? 's' : ''} honored)`
504
+ : '';
505
+ console.log(` [${adapter.name}] Applied defaults: ${resolved.applicableAllow.length} allow, ${resolved.applicableDeny.length} deny${removalNote}`);
396
506
  }
397
507
  }
508
+ if (team)
509
+ agentConfigsApplied++;
510
+ }
511
+ catch {
512
+ // Best-effort per adapter
398
513
  }
399
514
  }
400
- }
401
- catch {
402
- // Team config pull is non-fatal
403
- }
404
- // Ensure sandbox network access for agents that run in sandboxed environments (e.g. Cursor)
405
- const networkDomains = ['runwork.ai', '*.runwork.ai'];
406
- try {
407
- const baseHost = new URL(baseUrl).hostname;
408
- if (baseHost !== 'runwork.ai' && !baseHost.endsWith('.runwork.ai')) {
409
- networkDomains.push(baseHost, `*.${baseHost}`);
410
- }
411
- }
412
- catch { /* use defaults */ }
413
- for (const adapter of adapters) {
414
- if (!adapter.writeAgentConfig)
415
- continue;
416
- try {
417
- await adapter.writeAgentConfig({
418
- networkAllowlist: networkDomains,
419
- minimumPermissions: [
420
- { field: 'approval_policy', order: ['always', 'untrusted', 'on-request', 'never'], minimum: 'on-request' },
421
- { field: 'sandbox_mode', order: ['full', 'read-only', 'workspace-write', 'off'], minimum: 'workspace-write' },
422
- ],
423
- }, 'user');
424
- }
425
- catch {
426
- // Best-effort
427
- }
515
+ state.agentDefaultsVersion = AGENT_DEFAULTS_SCHEMA_VERSION;
428
516
  }
429
517
  // Register ~/.runwork as a project in the Codex desktop app (best-effort).
430
518
  // Only attempts when Codex adapter is configured and the desktop app is closed.