atris 3.56.0 → 3.56.2

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 (45) hide show
  1. package/atris/skills/engines/SKILL.md +11 -3
  2. package/atris/skills/x-search/SKILL.md +20 -1
  3. package/atris/skills/youtube/SKILL.md +51 -43
  4. package/bin/atris.js +134 -90
  5. package/commands/auth.js +6 -1
  6. package/commands/autopilot-front.js +29 -13
  7. package/commands/brainstorm.js +77 -476
  8. package/commands/business.js +13 -1
  9. package/commands/engine.js +7 -0
  10. package/commands/experiments.js +178 -0
  11. package/commands/fleet-report.js +2 -2
  12. package/commands/founder.js +12 -0
  13. package/commands/human-missions.js +64 -2
  14. package/commands/init.js +2 -35
  15. package/commands/integrations.js +100 -0
  16. package/commands/land.js +53 -23
  17. package/commands/later.js +52 -0
  18. package/commands/launchpad.js +45 -10
  19. package/commands/log.js +79 -30
  20. package/commands/mission.js +117 -22
  21. package/commands/next.js +153 -63
  22. package/commands/now.js +115 -38
  23. package/commands/recap.js +97 -4
  24. package/commands/run-front.js +20 -5
  25. package/commands/spaceship.js +52 -12
  26. package/commands/status.js +38 -7
  27. package/commands/task.js +56 -19
  28. package/commands/terminal.js +5 -5
  29. package/commands/workflow.js +19 -5
  30. package/commands/x-search.js +123 -13
  31. package/commands/youtube.js +941 -36
  32. package/lib/account-bound.js +7 -0
  33. package/lib/apply-gate.js +102 -0
  34. package/lib/config-guard.js +106 -0
  35. package/lib/context-gatherer.js +55 -8
  36. package/lib/engine-ask.js +16 -6
  37. package/lib/engine-registry.js +14 -4
  38. package/lib/first-minute.js +571 -26
  39. package/lib/known-commands.js +1 -1
  40. package/lib/pack-capabilities.js +13 -3
  41. package/lib/runner-command.js +5 -3
  42. package/lib/scratch-root.js +44 -0
  43. package/lib/workspace-scaffold.js +3 -1
  44. package/package.json +1 -1
  45. package/utils/auth.js +84 -6
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const knownCommands = ['init', 'doctor', 'install', 'workspace', 'log', 'logs', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
3
+ const knownCommands = ['init', 'doctor', 'install', 'workspace', 'log', 'logs', 'later', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
4
4
  'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
5
5
  'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
6
6
  'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const dns = require('dns').promises;
7
7
  const net = require('net');
8
8
  const { randomUUID } = require('crypto');
9
+ const { enforceConfigGuard } = require('./config-guard');
9
10
 
10
11
  const CAPABILITY_DEFINITIONS = Object.freeze({
11
12
  'pack.read': Object.freeze({
@@ -34,6 +35,7 @@ const TOOL_ORDER = Object.freeze([
34
35
  ]);
35
36
 
36
37
  const FILE_TOOLS = new Set(['Read', 'Glob', 'Grep', 'Edit', 'Write']);
38
+ const PRE_TOOL_USE_MATCHER = 'Read|Glob|Grep|Edit|Write|WebFetch|Bash';
37
39
  const CLAUDE_SESSION_END_REASONS = new Set([
38
40
  'clear',
39
41
  'resume',
@@ -211,7 +213,7 @@ function buildClaudeCapabilityArgs(policy, options = {}) {
211
213
  },
212
214
  hooks: {
213
215
  PreToolUse: [{
214
- matcher: 'Read|Glob|Grep|Edit|Write|WebFetch',
216
+ matcher: PRE_TOOL_USE_MATCHER,
215
217
  hooks: [{ type: 'command', command: `${hookCommand} pre` }],
216
218
  }],
217
219
  PostToolUse: [{
@@ -396,6 +398,7 @@ function beginPackRunReceipt(packDir, manifest, policy, options = {}) {
396
398
  runner: 'claude',
397
399
  builtInToolCeiling: true,
398
400
  packRootFileBoundary: true,
401
+ claudeSettingsGuard: true,
399
402
  preLaunchContextBoundary: true,
400
403
  declaredTreeSymlinksRejected: true,
401
404
  packOpeningSlashCommandsEscaped: true,
@@ -611,12 +614,18 @@ function denyPreToolUse(env, input, reason, now) {
611
614
  };
612
615
  }
613
616
 
617
+ function decidePreToolUse(input, env) {
618
+ const configDecision = enforceConfigGuard(input, { cwd: env.root });
619
+ if (!configDecision.allowed) return configDecision;
620
+ return enforcePackRoot(input, env.root);
621
+ }
622
+
614
623
  function runHook(mode, rawInput) {
615
624
  const env = hookEnvironment();
616
625
  const input = rawInput ? JSON.parse(rawInput) : {};
617
626
  const now = new Date().toISOString();
618
627
  if (mode === 'pre') {
619
- const decision = enforcePackRoot(input, env.root);
628
+ const decision = decidePreToolUse(input, env);
620
629
  if (!decision.allowed) {
621
630
  return denyPreToolUse(env, input, decision.reason, now);
622
631
  }
@@ -645,7 +654,7 @@ async function runHookAsync(mode, rawInput, options = {}) {
645
654
  const env = hookEnvironment();
646
655
  const input = rawInput ? JSON.parse(rawInput) : {};
647
656
  const now = new Date().toISOString();
648
- const fileDecision = enforcePackRoot(input, env.root);
657
+ const fileDecision = decidePreToolUse(input, env);
649
658
  if (!fileDecision.allowed) return denyPreToolUse(env, input, fileDecision.reason, now);
650
659
  const webDecision = await enforcePublicWeb(input, options);
651
660
  if (!webDecision.allowed) return denyPreToolUse(env, input, webDecision.reason, now);
@@ -677,6 +686,7 @@ module.exports = {
677
686
  finalizePackRunReceipt,
678
687
  receiptDirectory,
679
688
  classifyPackRunLifecycle,
689
+ enforceConfigGuard,
680
690
  enforcePackRoot,
681
691
  publicWebUrlPreflight,
682
692
  enforcePublicWeb,
@@ -9,6 +9,7 @@
9
9
  // first in precedence. Precedence: explicit model -> ATRIS_RUNNER_MODEL env ->
10
10
  // ATRIS_RUNNER_PROFILE -> legacy ATRIS_CLAUDE_MODEL env -> pinned default.
11
11
  const DEFAULT_CLAUDE_RUNNER_MODEL = 'claude-opus-4-8';
12
+ const DEFAULT_FABLE_RUNNER_MODEL = 'claude-fable-5';
12
13
  const DEFAULT_CLAUDE_RUNNER_BIN = 'claude';
13
14
 
14
15
  // Canonical runner profiles. Each entry is the config an operator would pick
@@ -40,11 +41,11 @@ const RUNNER_PROFILE_DEFS = Object.freeze({
40
41
  model: '',
41
42
  commandTemplate: '{bin} --trust -p {prompt}',
42
43
  }),
43
- // Compatibility names for fleet rosters. These map to real installed
44
- // runners instead of retired or UI-only model names.
44
+ // Fable is a distinct Claude Code model, not a friendly name for whatever
45
+ // Claude model happens to be selected locally.
45
46
  fable: Object.freeze({
46
47
  bin: 'claude',
47
- model: '',
48
+ model: DEFAULT_FABLE_RUNNER_MODEL,
48
49
  commandTemplate: '',
49
50
  }),
50
51
  composer: Object.freeze({
@@ -220,6 +221,7 @@ function buildRunnerCommand({ promptFile, allowedTools, model } = {}) {
220
221
 
221
222
  module.exports = {
222
223
  DEFAULT_CLAUDE_RUNNER_MODEL,
224
+ DEFAULT_FABLE_RUNNER_MODEL,
223
225
  DEFAULT_CLAUDE_RUNNER_BIN,
224
226
  RUNNER_PROFILES,
225
227
  RUNNER_PROFILE_DEFS,
@@ -43,6 +43,50 @@ function isGenericScratchRoot(dir) {
43
43
  }
44
44
  }
45
45
 
46
+ function keyUnderScratchRoot(key, roots) {
47
+ for (const root of roots) {
48
+ if (key === root || key.startsWith(`${root}/`)) return true;
49
+ }
50
+ return false;
51
+ }
52
+
53
+ function isUnderGenericScratchRoot(dir) {
54
+ if (!dir) return false;
55
+ const roots = genericScratchRoots();
56
+ const key = pathKey(dir);
57
+ if (keyUnderScratchRoot(key, roots)) return true;
58
+ try {
59
+ return keyUnderScratchRoot(pathKey(fs.realpathSync(dir)), roots);
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ function hasWorkspaceRoom(dir) {
66
+ const root = path.resolve(dir || '.');
67
+ return fs.existsSync(path.join(root, 'atris'))
68
+ || fs.existsSync(path.join(root, '.atris', 'business.json'));
69
+ }
70
+
71
+ // An empty child under /tmp (or another generic scratch root) is not a room.
72
+ // --yes is consent to start, not a workspace unlock. A workspace that already
73
+ // lives here (dogfood) still has atris/ or a business binding.
74
+ function isUnboundScratchFolder(dir) {
75
+ if (!isUnderGenericScratchRoot(dir)) return false;
76
+ return !hasWorkspaceRoom(dir);
77
+ }
78
+
79
+ const UNBOUND_SCRATCH_MESSAGE = 'this folder is not a room';
80
+
81
+ function refuseUnboundScratch(write = console.error) {
82
+ write(UNBOUND_SCRATCH_MESSAGE);
83
+ return 2;
84
+ }
85
+
46
86
  module.exports = {
87
+ UNBOUND_SCRATCH_MESSAGE,
47
88
  isGenericScratchRoot,
89
+ isUnboundScratchFolder,
90
+ isUnderGenericScratchRoot,
91
+ refuseUnboundScratch,
48
92
  };
@@ -147,7 +147,9 @@ function ensureWorkspaceBrain(projectRoot = process.cwd(), { now = new Date() }
147
147
  }
148
148
 
149
149
  const year = String(now.getFullYear());
150
- const today = now.toISOString().split('T')[0];
150
+ // Local calendar day, matching lib/file-ops getLogPath; toISOString is UTC
151
+ // and creates tomorrow's journal every evening west of Greenwich.
152
+ const today = `${year}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
151
153
  const journalFile = path.join(atrisDir, 'logs', year, `${today}.md`);
152
154
  if (writeIfMissing(journalFile, firstJournalMarkdown(today))) note(journalFile);
153
155
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.56.0",
3
+ "version": "3.56.2",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {
package/utils/auth.js CHANGED
@@ -86,6 +86,7 @@ function promptUser(question) {
86
86
  }
87
87
 
88
88
  const TOKEN_REFRESH_BUFFER_SECONDS = 300;
89
+ const AGENT_TOKEN_EXPIRED_DETAIL = 'Agent token expired. Mint a new one: atris login --agent';
89
90
 
90
91
  /**
91
92
  * Decode and parse the claims from a JWT token.
@@ -164,6 +165,48 @@ function getCredentialsPath() {
164
165
  return path.join(getAtrisDir(), 'credentials.json');
165
166
  }
166
167
 
168
+ function getPlacedAgentTokenPath() {
169
+ const override = process.env.ATRIS_AGENT_TOKEN_FILE;
170
+ if (override && override.trim()) return override.trim();
171
+ return path.join(os.homedir(), '.atris', 'agent-token.json');
172
+ }
173
+
174
+ function expiryTimeMs(value) {
175
+ if (typeof value === 'number' && Number.isFinite(value)) {
176
+ return value > 1e12 ? value : value * 1000;
177
+ }
178
+ if (typeof value !== 'string' || !value.trim()) return null;
179
+ const trimmed = value.trim();
180
+ if (/^\d+(?:\.\d+)?$/.test(trimmed)) {
181
+ const numeric = Number(trimmed);
182
+ if (Number.isFinite(numeric)) return numeric > 1e12 ? numeric : numeric * 1000;
183
+ }
184
+ const parsed = Date.parse(trimmed);
185
+ return Number.isFinite(parsed) ? parsed : null;
186
+ }
187
+
188
+ function loadPlacedAgentToken() {
189
+ try {
190
+ const parsed = JSON.parse(fs.readFileSync(getPlacedAgentTokenPath(), 'utf8'));
191
+ const token = typeof parsed?.token === 'string' ? parsed.token.trim() : '';
192
+ if (!token || expiryTimeMs(parsed?.expires_at) === null) return null;
193
+ return {
194
+ token,
195
+ expires_at: parsed.expires_at,
196
+ scopes: Array.isArray(parsed.scopes) ? parsed.scopes : [],
197
+ provider: null,
198
+ source: 'agent_token_file',
199
+ };
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ function isUnexpiredCredential(credentials) {
206
+ const expiresAt = expiryTimeMs(credentials?.expires_at);
207
+ return expiresAt !== null && expiresAt > Date.now();
208
+ }
209
+
167
210
  function getSessionsDir() {
168
211
  return mkPrivateDir(path.join(getAtrisDir(), 'sessions'));
169
212
  }
@@ -332,17 +375,36 @@ function saveCredentials(token, refreshToken, email, userId, provider) {
332
375
  }
333
376
 
334
377
  function loadCredentials() {
335
- // Priority: ATRIS_TOKEN env var → ATRIS_PROFILE env varper-terminal session file → global credentials.json
378
+ // Priority: ATRIS_TOKEN env var → placed agent tokenATRIS_PROFILE env var
379
+ // → per-terminal session file → global credentials.json. A fresh placed token
380
+ // overrides a different env token because cloud images can carry a stale one.
336
381
 
337
382
  // 0. Raw token injection. Headless boxes (cloud business computers) have no
338
383
  // browser for `atris login`; the runner injects a scoped token as env
339
384
  // instead, so no credentials file ever lands on disk.
340
385
  const envToken = process.env.ATRIS_TOKEN;
341
- if (envToken && envToken.trim()) {
342
- return { token: envToken.trim(), provider: null, source: 'env' };
386
+ const normalizedEnvToken = envToken && envToken.trim();
387
+ const placedAgentToken = loadPlacedAgentToken();
388
+ if (
389
+ normalizedEnvToken &&
390
+ placedAgentToken &&
391
+ placedAgentToken.token !== normalizedEnvToken &&
392
+ isUnexpiredCredential(placedAgentToken)
393
+ ) {
394
+ return placedAgentToken;
395
+ }
396
+ if (normalizedEnvToken) {
397
+ return { token: normalizedEnvToken, provider: null, source: 'env' };
398
+ }
399
+
400
+ // 1. Backend-placed key for per-user cloud computers. Missing, malformed,
401
+ // incomplete, and expired files are ignored so later sources keep their
402
+ // existing order.
403
+ if (placedAgentToken && isUnexpiredCredential(placedAgentToken)) {
404
+ return placedAgentToken;
343
405
  }
344
406
 
345
- // 1. Explicit env var override
407
+ // 2. Explicit env var override
346
408
  const profileOverride = process.env.ATRIS_PROFILE;
347
409
  if (profileOverride) {
348
410
  const profile = loadProfile(profileOverride);
@@ -358,14 +420,14 @@ function loadCredentials() {
358
420
  }
359
421
  }
360
422
 
361
- // 2. Per-terminal session override (set by atris switch)
423
+ // 3. Per-terminal session override (set by atris switch)
362
424
  const sessionProfile = getSessionProfile();
363
425
  if (sessionProfile) {
364
426
  const profile = loadProfile(sessionProfile);
365
427
  if (profile) return { ...profile, source_profile: sessionProfile };
366
428
  }
367
429
 
368
- // 3. Global credentials.json
430
+ // 4. Global credentials.json
369
431
  const credentialsPath = getCredentialsPath();
370
432
 
371
433
  if (!fs.existsSync(credentialsPath)) {
@@ -546,6 +608,20 @@ async function ensureValidCredentials(apiRequestJson, options = {}) {
546
608
  return { error: 'not_logged_in' };
547
609
  }
548
610
 
611
+ const selectedExpiry = expiryTimeMs(credentials.expires_at);
612
+ if (selectedExpiry !== null && selectedExpiry <= Date.now()) {
613
+ return { error: 'token_invalid', detail: AGENT_TOKEN_EXPIRED_DETAIL };
614
+ }
615
+
616
+ const claims = decodeJwtClaims(credentials.token);
617
+ if (claims?.type === 'agent_access') {
618
+ if (typeof claims.exp === 'number' && claims.exp <= Math.floor(Date.now() / 1000)) {
619
+ return { error: 'token_invalid', detail: AGENT_TOKEN_EXPIRED_DETAIL };
620
+ }
621
+ // Server-side scope enforcement on every real request is unchanged and remains the actual security boundary.
622
+ return { credentials, user: null, source: 'agent_token' };
623
+ }
624
+
549
625
  if (credentials.refresh_token && shouldRefreshToken(credentials.token)) {
550
626
  const proactive = await performTokenRefresh(credentials, apiRequestJson);
551
627
  if (proactive.ok) {
@@ -563,6 +639,7 @@ async function ensureValidCredentials(apiRequestJson, options = {}) {
563
639
 
564
640
  if (
565
641
  credentials.source !== 'env' &&
642
+ credentials.source !== 'agent_token_file' &&
566
643
  (updatedEmail !== credentials.email ||
567
644
  updatedProvider !== credentials.provider ||
568
645
  updatedUserId !== credentials.user_id)
@@ -671,6 +748,7 @@ async function displayAccountSummary(apiRequestJson) {
671
748
  }
672
749
 
673
750
  module.exports = {
751
+ AGENT_TOKEN_EXPIRED_DETAIL,
674
752
  decodeJwtClaims,
675
753
  getTokenExpiryEpochSeconds,
676
754
  shouldRefreshToken,