@arjunkhera/atlas 0.3.0 → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, for any repository.",
5
5
  "author": {
6
6
  "name": "Arjun Khera"
package/door/cli.mjs CHANGED
@@ -51,6 +51,10 @@ Atlas ${ATLAS_VERSION}. Check version ${CHECK_VERSION}; it reads spec ${Object.k
51
51
  A person merges every file the tooling writer writes.
52
52
  `;
53
53
 
54
+ // A switch never takes the next word as its value, so it may come before a
55
+ // path: `atlas ste --share page.md` reads page.md (a miss on 6 October).
56
+ const SWITCHES = new Set(['help', 'share', 'replace-unknown', 'workflow', 'dry-run', 'local', 'skip-global', 'yes']);
57
+
54
58
  function options(argv) {
55
59
  const out = { _: [] };
56
60
  for (let index = 0; index < argv.length; index += 1) {
@@ -58,7 +62,7 @@ function options(argv) {
58
62
  if (!flag.startsWith('--')) { out._.push(flag); continue; }
59
63
  const key = flag.slice(2);
60
64
  const value = argv[index + 1];
61
- if (value === undefined || value.startsWith('--')) { out[key] = true; continue; }
65
+ if (SWITCHES.has(key) || value === undefined || value.startsWith('--')) { out[key] = true; continue; }
62
66
  out[key] = value;
63
67
  index += 1;
64
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, as a Claude Code plugin for any repository.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/work/mcp.mjs CHANGED
@@ -6,11 +6,14 @@
6
6
  // Row F11 (amendment A1.9): the server ships in the Atlas plugin. Claude Code
7
7
  // starts it from the plugin's .mcp.json, which sets every variable it reads.
8
8
  // The Engram address and key arrive together, as one setting, in
9
- // ATLAS_ENGRAM. The server reads them from nowhere else.
9
+ // ATLAS_ENGRAM. When that setting arrives empty, the server reads the same
10
+ // value from one owner-only file, ~/.config/atlas/engram-connection (atlas#101:
11
+ // Claude Desktop starts the server with the sensitive setting empty). It reads
12
+ // them from nowhere else.
10
13
  import { execFileSync } from 'node:child_process';
11
14
  import { randomUUID } from 'node:crypto';
12
15
  import { resolve, isAbsolute, dirname, join, delimiter } from 'node:path';
13
- import { existsSync, realpathSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
16
+ import { existsSync, realpathSync, readFileSync, writeFileSync, mkdirSync, statSync, openSync, fstatSync, closeSync, constants as fsConstants } from 'node:fs';
14
17
  import { fileURLToPath } from 'node:url';
15
18
  import { createEngramClient } from './lib/client.mjs';
16
19
  import { createActionStore } from './lib/action-store.mjs';
@@ -70,9 +73,48 @@ export const TOOLS = [
70
73
  // It never throws. A missing, malformed or expired connection is a state, and
71
74
  // the server still starts and lists its tools; each tool then answers with the
72
75
  // sentence below (fix six of the F11 study).
73
- const CONNECTION_HELP = 'Set it in the Atlas plugin settings, as "https://<host> <key>".';
76
+ //
77
+ // The setting wins. The file is read only when the setting is empty, and only
78
+ // when it is a plain file that the owner owns and no one else can read. The
79
+ // server opens it once with O_NOFOLLOW and checks that open file, so no swap
80
+ // can slip in between the check and the read. A read error is a refusal, never
81
+ // a throw.
82
+ const CONNECTION_HELP = 'Set it in the Atlas plugin settings, or in the file ~/.config/atlas/engram-connection, as "https://<host> <key>".';
83
+ const CONNECTION_FILE_LIMIT = 16 * 1024;
84
+ export function connectionFile(env) { return env.HOME ? join(env.HOME, '.config', 'atlas', 'engram-connection') : null; }
85
+ function readConnectionFile(env) {
86
+ const path = connectionFile(env);
87
+ if (!path) return { raw: '' };
88
+ const owner = typeof process.getuid === 'function' ? process.getuid() : null;
89
+ let fd;
90
+ try {
91
+ const folder = statSync(dirname(path), { throwIfNoEntry: false });
92
+ if (!folder) return { raw: '' };
93
+ if ((owner !== null && folder.uid !== owner) || folder.mode & 0o022) return { refused: `Other users can change ${dirname(path)}. Run: chmod 700 ${dirname(path)}` };
94
+ try { fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK); } catch (error) {
95
+ if (error.code === 'ENOENT') return { raw: '' };
96
+ if (error.code === 'ELOOP') return { refused: `${path} is not a plain file.` };
97
+ throw error;
98
+ }
99
+ const info = fstatSync(fd);
100
+ if (!info.isFile()) return { refused: `${path} is not a plain file.` };
101
+ if (owner !== null && info.uid !== owner) return { refused: `${path} belongs to another user.` };
102
+ if (info.mode & 0o077) return { refused: `Other users can read ${path}. Run: chmod 600 ${path}` };
103
+ if (info.size > CONNECTION_FILE_LIMIT) return { refused: `${path} is too large to be a connection line.` };
104
+ return { raw: readFileSync(fd, 'utf8').trim() };
105
+ } catch (error) {
106
+ return { refused: `Atlas could not read ${path} (${error.code ?? 'error'}).` };
107
+ } finally {
108
+ if (fd !== undefined) closeSync(fd);
109
+ }
110
+ }
74
111
  export function readConnection(env, { allowLoopbackHttp = false, now = Date.now() } = {}) {
75
- const raw = unset(env.ATLAS_ENGRAM) ? '' : String(env.ATLAS_ENGRAM).trim();
112
+ let raw = unset(env.ATLAS_ENGRAM) ? '' : String(env.ATLAS_ENGRAM).trim();
113
+ if (!raw) {
114
+ const file = readConnectionFile(env);
115
+ if (file.refused) return { state: 'bad', message: `The Engram connection file is refused. ${file.refused}` };
116
+ raw = file.raw;
117
+ }
76
118
  if (!raw) return { state: 'missing', message: `No Engram connection is set. ${CONNECTION_HELP}` };
77
119
  const parts = raw.split(/\s+/);
78
120
  if (parts.length !== 2) return { state: 'bad', message: `The Engram connection is not in the form "https://<host> <key>". ${CONNECTION_HELP}` };
@@ -87,7 +129,7 @@ export function readConnection(env, { allowLoopbackHttp = false, now = Date.now(
87
129
  if (claims.principal_type !== 'user' || !claims.jti || !claims.exp) return { state: 'bad', message: `The Engram key must be a user token with a token id. ${CONNECTION_HELP}` };
88
130
  if (claims.exp <= now / 1000) {
89
131
  const day = new Date(claims.exp * 1000).toISOString().slice(0, 10);
90
- return { state: 'expired', message: `The Engram key expired on ${day}. Enter a new one in the Atlas plugin settings.`, expired: day };
132
+ return { state: 'expired', message: `The Engram key expired on ${day}. Enter a new one in the Atlas plugin settings, or in ~/.config/atlas/engram-connection.`, expired: day };
91
133
  }
92
134
  return { state: 'ok', url: url.origin, token, tokenId: claims.jti, expires: new Date(claims.exp * 1000).toISOString().slice(0, 10) };
93
135
  }