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.
- package/atris/skills/engines/SKILL.md +11 -3
- package/atris/skills/x-search/SKILL.md +20 -1
- package/atris/skills/youtube/SKILL.md +51 -43
- package/bin/atris.js +134 -90
- package/commands/auth.js +6 -1
- package/commands/autopilot-front.js +29 -13
- package/commands/brainstorm.js +77 -476
- package/commands/business.js +13 -1
- package/commands/engine.js +7 -0
- package/commands/experiments.js +178 -0
- package/commands/fleet-report.js +2 -2
- package/commands/founder.js +12 -0
- package/commands/human-missions.js +64 -2
- package/commands/init.js +2 -35
- package/commands/integrations.js +100 -0
- package/commands/land.js +53 -23
- package/commands/later.js +52 -0
- package/commands/launchpad.js +45 -10
- package/commands/log.js +79 -30
- package/commands/mission.js +117 -22
- package/commands/next.js +153 -63
- package/commands/now.js +115 -38
- package/commands/recap.js +97 -4
- package/commands/run-front.js +20 -5
- package/commands/spaceship.js +52 -12
- package/commands/status.js +38 -7
- package/commands/task.js +56 -19
- package/commands/terminal.js +5 -5
- package/commands/workflow.js +19 -5
- package/commands/x-search.js +123 -13
- package/commands/youtube.js +941 -36
- package/lib/account-bound.js +7 -0
- package/lib/apply-gate.js +102 -0
- package/lib/config-guard.js +106 -0
- package/lib/context-gatherer.js +55 -8
- package/lib/engine-ask.js +16 -6
- package/lib/engine-registry.js +14 -4
- package/lib/first-minute.js +571 -26
- package/lib/known-commands.js +1 -1
- package/lib/pack-capabilities.js +13 -3
- package/lib/runner-command.js +5 -3
- package/lib/scratch-root.js +44 -0
- package/lib/workspace-scaffold.js +3 -1
- package/package.json +1 -1
- package/utils/auth.js +84 -6
package/lib/known-commands.js
CHANGED
|
@@ -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',
|
package/lib/pack-capabilities.js
CHANGED
|
@@ -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:
|
|
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 =
|
|
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 =
|
|
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,
|
package/lib/runner-command.js
CHANGED
|
@@ -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
|
-
//
|
|
44
|
-
//
|
|
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,
|
package/lib/scratch-root.js
CHANGED
|
@@ -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
|
-
|
|
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
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 →
|
|
378
|
+
// Priority: ATRIS_TOKEN env var → placed agent token → ATRIS_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
|
-
|
|
342
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|