atris 3.56.1 → 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.
@@ -8,6 +8,7 @@ const { isGenericScratchRoot } = require('./scratch-root');
8
8
 
9
9
  const FIRST_USE_COMMAND = 'atris "help me choose the first useful step for this project"';
10
10
  const FIRST_TALK_ASK = 'what do you want here';
11
+ const CLAIMED_KEEP_WORKING_VERIFY = 'git diff --check';
11
12
  const LATER_NOTE_FILE = '.atris-later';
12
13
  const BARE_ATRIS_FLAGS = new Set(['--yes', '-y', '--json', '--verbose']);
13
14
  const NOT_A_PERSON = new Set([
@@ -237,6 +238,17 @@ function loadLatestRecap(root = process.cwd()) {
237
238
  return title ? { title, file: hit.file } : null;
238
239
  }
239
240
 
241
+ function claimedKeepWorkingCommand(task) {
242
+ const ref = taskRef(task);
243
+ return ref
244
+ ? `atris task ready ${ref} --verify "${CLAIMED_KEEP_WORKING_VERIFY}"`
245
+ : `atris task ready --verify "${CLAIMED_KEEP_WORKING_VERIFY}"`;
246
+ }
247
+
248
+ function isClaimedKeepWorkingCommand(command) {
249
+ return /^atris task ready\b/.test(String(command || ''));
250
+ }
251
+
240
252
  function taskCommand(task, person) {
241
253
  const ref = taskRef(task);
242
254
  const who = person || 'operator';
@@ -244,7 +256,7 @@ function taskCommand(task, person) {
244
256
  if (task.status === 'review') {
245
257
  return ref ? `atris task accept ${ref}` : 'atris task reviews --limit 5';
246
258
  }
247
- if (task.status === 'claimed') return 'atris do';
259
+ if (task.status === 'claimed') return claimedKeepWorkingCommand(task);
248
260
  if (task.status === 'open') {
249
261
  return ref ? `atris task claim ${ref} --as ${who}` : 'atris task next';
250
262
  }
@@ -855,7 +867,13 @@ function spokenWinReason(text) {
855
867
  }
856
868
 
857
869
  function isKeepWorkingMinute(minute) {
858
- return Boolean(minute && minute.nextCommand === 'atris do');
870
+ const next = String((minute && minute.nextCommand) || '');
871
+ if (next === 'atris do') return true;
872
+ return isClaimedKeepWorkingCommand(next);
873
+ }
874
+
875
+ function isClaimMinute(minute) {
876
+ return Boolean(minute && /^atris task claim\b/.test(minute.nextCommand || ''));
859
877
  }
860
878
 
861
879
  function speakKeepWorkingMinute({
@@ -918,6 +936,7 @@ module.exports = {
918
936
  spokenLineCount,
919
937
  spokenWinReason,
920
938
  isKeepWorkingMinute,
939
+ isClaimMinute,
921
940
  taskCommand,
922
941
  taskNextCommand,
923
942
  writeLaterNote,
@@ -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,
@@ -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.1",
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,10 +608,15 @@ 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
+
549
616
  const claims = decodeJwtClaims(credentials.token);
550
617
  if (claims?.type === 'agent_access') {
551
618
  if (typeof claims.exp === 'number' && claims.exp <= Math.floor(Date.now() / 1000)) {
552
- return { error: 'token_invalid', detail: 'Agent token expired. Mint a new one: atris login --agent' };
619
+ return { error: 'token_invalid', detail: AGENT_TOKEN_EXPIRED_DETAIL };
553
620
  }
554
621
  // Server-side scope enforcement on every real request is unchanged and remains the actual security boundary.
555
622
  return { credentials, user: null, source: 'agent_token' };
@@ -572,6 +639,7 @@ async function ensureValidCredentials(apiRequestJson, options = {}) {
572
639
 
573
640
  if (
574
641
  credentials.source !== 'env' &&
642
+ credentials.source !== 'agent_token_file' &&
575
643
  (updatedEmail !== credentials.email ||
576
644
  updatedProvider !== credentials.provider ||
577
645
  updatedUserId !== credentials.user_id)
@@ -680,6 +748,7 @@ async function displayAccountSummary(apiRequestJson) {
680
748
  }
681
749
 
682
750
  module.exports = {
751
+ AGENT_TOKEN_EXPIRED_DETAIL,
683
752
  decodeJwtClaims,
684
753
  getTokenExpiryEpochSeconds,
685
754
  shouldRefreshToken,