@knowttl/artium-axi 0.0.0 → 0.0.3

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/README.md CHANGED
@@ -20,7 +20,7 @@ See the [evidence ledger](docs/evidence-ledger.md) for the acceptance boundaries
20
20
  ## Agent Ergonomics
21
21
 
22
22
  - Running without arguments shows identity, roles, available tools, cached health and pinned versions.
23
- - Tool lists use a compact `name`, `status`, `version` schema and suggest a detail view and first call.
23
+ - Tool lists show cached health, pinned versions and write-access status and suggest a detail view and first call.
24
24
  - Empty lists explain why no enabled tool is available and direct you to an administrator.
25
25
  - Local help works before sign-in and includes usage, purpose, flags and an example.
26
26
  - Artium errors use TOON on stdout; upstream calls preserve separate stdout, stderr and exit status.
@@ -369,6 +369,7 @@ After signing in, run `node dist/cli/artium-axi.mjs setup --host codex` to insta
369
369
  The root package remains private for server development; `pnpm build:npm` stages the separate public client package in `dist/npm` using the root package version.
370
370
  Run `npm pack --dry-run ./dist/npm` to inspect the client tarball before release.
371
371
  Only the bundled CLI, skill, README, LICENSE, NOTICE and client package metadata are included.
372
+ See [npm client releases](docs/runbook.md#npm-client-releases) for automated publishing and owner setup.
372
373
 
373
374
  ## Usage
374
375
 
@@ -377,6 +378,7 @@ Only the bundled CLI, skill, README, LICENSE, NOTICE and client package metadata
377
378
  | Command | Purpose |
378
379
  | --- | --- |
379
380
  | `artium-axi` | Show identity, roles and available tools. |
381
+ | `artium-axi --version` | Print only the client package version and a newline, then exit without configuration, sign-in or network access. |
380
382
  | `artium-axi login --username synthetic-user` | Sign in with a local account. |
381
383
  | `artium-axi login --sso` | Sign in through browser approval with an enabled SSO provider; works on headless and SSH hosts. |
382
384
  | `artium-axi passwd` | Change your password, then sign in again. |
@@ -411,9 +413,8 @@ Other Azure commands, including the dashboard and `home`, leave stdin unread; he
411
413
  Local upload paths become leaf names in the request; duplicate leaf names and oversized input are refused.
412
414
  The API supports bounded stdin and named uploads through the [shared API contract](src/shared/api.ts).
413
415
 
414
- Write permission remains subject to the pinned tool's native gates and the configured adapter policy.
415
- Vectra writes require its configured allowlist and upstream confirmation; Artium does not add a universal confirmation flag.
416
- Falcon has no editable native write settings.
416
+ See [write-access administration](docs/runbook.md#roles-credentials-and-health) for per-tool switches, native write gates and the current Falcon limitation.
417
+ Artium does not add a universal confirmation flag.
417
418
 
418
419
  Azure writes whose outcome is unknown are not retried and return `UNKNOWN_OUTCOME` with state-check guidance; reads may still retry.
419
420
  Artium also returns this error when its runner times out, cancels or receives a process signal after starting an executed Azure write; read timeouts remain `TIMEOUT`.
@@ -659,11 +660,12 @@ See the [API contract](src/shared/api.ts) and [retention and export runbook](doc
659
660
 
660
661
  Use `login --password-stdin` or `passwd --password-stdin` to supply the login or new password through stdin.
661
662
  Stdin removes one final LF or CRLF and preserves all other spaces and Unicode.
662
- Password arguments are refused; tokens are never printed.
663
+ Password arguments are refused; tokens are never printed to stdout or stderr.
663
664
  Ordinary password changes also require the current password through the terminal prompt or `ARTIUM_AXI_CURRENT_PASSWORD`.
664
665
  Clear password environment values after use.
665
666
 
666
- Sessions live in `artium-axi/session.json` under the applicable base directory and are bound to the normalized server URL.
667
+ By default, sessions live in `artium-axi/session.json` under the applicable base directory and are bound to the normalized server URL.
668
+ Callers that manage their own tokens can instead use [externally managed client sessions](#externally-managed-client-sessions).
667
669
  POSIX directories use mode 0700 and files mode 0600; Windows uses a restricted ACL and refuses storage if it cannot establish it.
668
670
  Do not export or share the session file.
669
671
  CLI processes serialize refreshes through `session.lock`; after `SESSION_BUSY`, remove that lock directory only when every CLI process has exited.
@@ -674,6 +676,46 @@ Certificate verification remains enforced even with `NODE_TLS_REJECT_UNAUTHORIZE
674
676
  Only an explicit pre-execution `TOKEN_EXPIRED` response permits one serialized refresh and retry.
675
677
  A network failure or uncertain command outcome is never retried automatically.
676
678
 
679
+ ### Externally managed client sessions
680
+
681
+ Callers that keep credentials outside the CLI can opt in with one leading flag: `artium-axi --managed-session <command> <args>`.
682
+ The caller supplies inherited pipes or sockets: fd 3 for session input and fd 4 for private rotation output.
683
+ Stdin remains available for tool input; stdout and stderr retain their normal command formats.
684
+ Do not pass tokens in arguments or environment variables, connect fd 4 to a log or public output, or redirect either private channel to a disk file.
685
+ The CLI refuses regular files as private channels and never reads, creates, locks, updates or deletes `session.json` in this mode.
686
+
687
+ Send one UTF-8 JSON object of at most 64 KiB to fd 3, then close the input pipe:
688
+
689
+ ```json
690
+ {"serverUrl":"https://artium.example.test","accessToken":"CANARY-access","refreshToken":"CANARY-refresh"}
691
+ ```
692
+
693
+ `serverUrl` must match the normalized `ARTIUM_AXI_URL`, allowing a final slash.
694
+ `accessToken` is required and nonempty; `refreshToken` is optional and nonempty when supplied.
695
+ The optional fields `status: "authenticated"`, `accessExpiresAt` and `familyExpiresAt` (integer Unix milliseconds) allow a rotation record to be reused as input; other fields are refused.
696
+ The caller owns token acquisition, expiry policy, refresh serialization across processes and secure persistence.
697
+ Managed mode supports the home view, `whoami`, `tools`, `tools show`, `doctor` and tool commands.
698
+ `login`, `passwd`, `logout` and `setup` are refused before any network call; ordinary sign-in and its server-version warning remain unchanged without the flag.
699
+ Help is available without private channels through `artium-axi --help`.
700
+
701
+ Only an explicit `TOKEN_EXPIRED` response permits a single refresh and retry of that request.
702
+ Without a refresh token, or if refresh is refused, the CLI returns the authentication error for the caller to handle.
703
+ Network failures and uncertain outcomes are never automatically retried.
704
+ After a successful refresh, fd 4 receives one newline-terminated JSON object before the request is retried:
705
+
706
+ ```json
707
+ {"serverUrl":"https://artium.example.test","status":"authenticated","accessToken":"CANARY-new-access","refreshToken":"CANARY-new-refresh","accessExpiresAt":1800000000000,"familyExpiresAt":1800003600000}
708
+ ```
709
+
710
+ Every rotation is delivered even if the subsequent request fails; an invocation without rotation writes no record.
711
+ Consume fd 4 concurrently with stdout and stderr, and retain rotations regardless of the command's exit status.
712
+ If rotation delivery fails, the CLI refuses to retry the request and returns `INVALID_INPUT` (exit 2); the caller must recover the session rather than reuse the consumed refresh token.
713
+ Managed public output is buffered and scanned against supplied and rotated tokens and their canonical encodings before printing; a match withholds both streams and returns `SECRET_IN_OUTPUT`.
714
+
715
+ Exit codes are 0 for success, 2 for invalid input, client configuration or private channels and refused commands, and 1 for authentication, server-binding, transport or output-scanning failures.
716
+ Tool exit codes pass through as usual.
717
+ Missing or closed descriptors, invalid JSON and mismatched server bindings fail before network access; no failure falls back to a session file.
718
+
677
719
  ### Server settings and custody
678
720
 
679
721
  | Variable | Meaning |
@@ -4498,9 +4498,9 @@ var $ZodTransform = /* @__PURE__ */ $constructor("$ZodTransform", (inst, def) =>
4498
4498
  }
4499
4499
  const _out = def.transform(payload.value, payload);
4500
4500
  if (ctx.async) {
4501
- const output2 = _out instanceof Promise ? _out : Promise.resolve(_out);
4502
- return output2.then((output3) => {
4503
- payload.value = output3;
4501
+ const output3 = _out instanceof Promise ? _out : Promise.resolve(_out);
4502
+ return output3.then((output4) => {
4503
+ payload.value = output4;
4504
4504
  return payload;
4505
4505
  });
4506
4506
  }
@@ -4950,12 +4950,12 @@ var $ZodFunction = /* @__PURE__ */ $constructor("$ZodFunction", (inst, def) => {
4950
4950
  output: inst._def.output
4951
4951
  });
4952
4952
  };
4953
- inst.output = (output2) => {
4953
+ inst.output = (output3) => {
4954
4954
  const F = inst.constructor;
4955
4955
  return new F({
4956
4956
  type: "function",
4957
4957
  input: inst._def.input,
4958
- output: output2
4958
+ output: output3
4959
4959
  });
4960
4960
  };
4961
4961
  return inst;
@@ -18364,14 +18364,14 @@ var ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
18364
18364
  payload.issues.push(util_exports.issue(_issue));
18365
18365
  }
18366
18366
  };
18367
- const output2 = def.transform(payload.value, payload);
18368
- if (output2 instanceof Promise) {
18369
- return output2.then((output3) => {
18370
- payload.value = output3;
18367
+ const output3 = def.transform(payload.value, payload);
18368
+ if (output3 instanceof Promise) {
18369
+ return output3.then((output4) => {
18370
+ payload.value = output4;
18371
18371
  return payload;
18372
18372
  });
18373
18373
  }
18374
- payload.value = output2;
18374
+ payload.value = output3;
18375
18375
  return payload;
18376
18376
  };
18377
18377
  });
@@ -20048,7 +20048,7 @@ var nativeSettingsSchemas = {
20048
20048
  allowWrites: external_exports.boolean(),
20049
20049
  operations: external_exports.array(external_exports.string().min(1).max(256)).min(1).max(64)
20050
20050
  }).optional() }),
20051
- "falcon-axi": external_exports.strictObject({}),
20051
+ "falcon-axi": external_exports.strictObject({ allowWrites: external_exports.boolean().optional() }),
20052
20052
  "vectra-axi": external_exports.strictObject({ writes: external_exports.strictObject({
20053
20053
  allowWrites: external_exports.boolean(),
20054
20054
  operations: external_exports.array(external_exports.string().min(1).refine((value) => value.trim().length > 0)).min(1)
@@ -20063,6 +20063,7 @@ var nativeSettingsSchemas = {
20063
20063
  };
20064
20064
  var healthSchema = external_exports.strictObject({
20065
20065
  status: external_exports.enum(["unconfigured", "unknown", "ok", "error", "disabled"]),
20066
+ readOnly: external_exports.boolean().default(true),
20066
20067
  checkedAt: external_exports.number().int().nullable(),
20067
20068
  credentialRevision: external_exports.number().int().nullable(),
20068
20069
  errorCode: external_exports.enum(Object.keys(errors)).nullable()
@@ -20071,6 +20072,7 @@ var adminToolSchema = external_exports.strictObject({
20071
20072
  name: toolSchema,
20072
20073
  enabled: external_exports.boolean(),
20073
20074
  version: external_exports.string(),
20075
+ readOnly: external_exports.boolean().default(true),
20074
20076
  nativeSettings: external_exports.record(external_exports.string(), external_exports.unknown()),
20075
20077
  revision: external_exports.number().int().positive(),
20076
20078
  updatedAt: external_exports.number().int().nullable(),
@@ -20394,6 +20396,7 @@ var toolsResponseSchema = external_exports.strictObject({
20394
20396
  tools: external_exports.array(external_exports.strictObject({
20395
20397
  name: toolSchema,
20396
20398
  version: external_exports.string(),
20399
+ readOnly: external_exports.boolean().default(true),
20397
20400
  status: external_exports.enum(["unconfigured", "unknown", "ok", "error", "disabled"])
20398
20401
  }))
20399
20402
  });
@@ -20449,7 +20452,7 @@ import { rootCertificates } from "node:tls";
20449
20452
  // package.json
20450
20453
  var package_default = {
20451
20454
  name: "artium-axi",
20452
- version: "0.0.0",
20455
+ version: "0.0.3",
20453
20456
  private: true,
20454
20457
  license: "MIT",
20455
20458
  type: "module",
@@ -20633,20 +20636,27 @@ var SessionStore = class {
20633
20636
  // src/cli/http.ts
20634
20637
  var DEFAULT_REQUEST_DEADLINE_MS = 9e4;
20635
20638
  var Client = class {
20636
- constructor(environment = process.env) {
20639
+ constructor(environment = process.env, managed2) {
20637
20640
  this.environment = environment;
20641
+ this.managed = managed2;
20638
20642
  try {
20639
20643
  const url2 = new URL(environment.ARTIUM_AXI_URL);
20640
20644
  if (url2.username || url2.password || url2.href.includes("?") || url2.href.includes("#") || url2.protocol !== "https:" && !(url2.protocol === "http:" && environment.ARTIUM_AXI_TEST_HTTP === "1" && ["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname))) throw new Error();
20641
20645
  this.serverUrl = url2.href.replace(/\/$/, "");
20642
- this.sessions = new SessionStore(this.serverUrl, sessionPath(process.platform, environment));
20646
+ if (!managed2) this.storedSessions = new SessionStore(this.serverUrl, sessionPath(process.platform, environment));
20643
20647
  } catch {
20644
20648
  throw new ArtiumError("CLIENT_CONFIG_INVALID");
20645
20649
  }
20650
+ managed2?.checkServer(this.serverUrl);
20646
20651
  }
20647
20652
  environment;
20653
+ managed;
20648
20654
  serverUrl;
20649
- sessions;
20655
+ storedSessions;
20656
+ get sessions() {
20657
+ if (!this.storedSessions) throw new ArtiumError("INVALID_INPUT", "Session-file operations are unavailable in managed mode.");
20658
+ return this.storedSessions;
20659
+ }
20650
20660
  certificateVerified = false;
20651
20661
  async request(path, method, input2, token) {
20652
20662
  this.certificateVerified = false;
@@ -20722,6 +20732,10 @@ var Client = class {
20722
20732
  return result.body;
20723
20733
  }
20724
20734
  async authenticated(path, method, input2, previous) {
20735
+ if (this.managed) return this.managed.authenticated(
20736
+ (token) => this.request(path, method, input2, token),
20737
+ (refreshToken) => this.request("/v1/auth/refresh", "POST", { refreshToken })
20738
+ );
20725
20739
  const session = previous ? this.reload(previous) : this.sessions.read();
20726
20740
  if (!session) throw new ArtiumError("TOKEN_INVALID", void 0, "Run artium-axi login --username <username> --password-stdin.");
20727
20741
  if (session.status === "password-change-required") throw new ArtiumError("PASSWORD_CHANGE_REQUIRED");
@@ -20805,16 +20819,18 @@ async function readPassword(stdin, environmentVariable) {
20805
20819
  }
20806
20820
 
20807
20821
  // src/cli/doctor.ts
20808
- async function doctor() {
20822
+ async function doctor(createClient = () => new Client(), write = (text2) => {
20823
+ process.stdout.write(text2);
20824
+ }) {
20809
20825
  let failure2;
20810
20826
  const line = (name, pass, detail, error62) => {
20811
20827
  if (!pass) failure2 ??= error62 ?? new ArtiumError("INVALID_RESPONSE");
20812
- process.stdout.write(`${pass ? "pass" : "fail"}: ${name}${detail ? ` (${detail})` : ""}
20828
+ write(`${pass ? "pass" : "fail"}: ${name}${detail ? ` (${detail})` : ""}
20813
20829
  `);
20814
20830
  };
20815
20831
  let client;
20816
20832
  try {
20817
- client = new Client();
20833
+ client = createClient();
20818
20834
  } catch (error62) {
20819
20835
  for (const name of ["server address", "certificate", "token", "CLI version"]) line(name, false, "CLIENT_CONFIG_INVALID");
20820
20836
  throw error62;
@@ -22830,7 +22846,7 @@ var ldap_axi_default = {
22830
22846
 
22831
22847
  // src/cli/help.ts
22832
22848
  var commands = {
22833
- home: { usage: "artium-axi [tools [show <tool>]|whoami|login|passwd|logout|doctor|setup|<tool> <args>]", purpose: "Show your identity, roles and available tools.", flags: [], example: "artium-axi" },
22849
+ home: { usage: "artium-axi [tools [show <tool>]|whoami|login|passwd|logout|doctor|setup|<tool> <args>]", purpose: "Show your identity, roles and available tools.", flags: ["--version (default false; print the client package version and exit)"], example: "artium-axi" },
22834
22850
  setup: {
22835
22851
  usage: "artium-axi setup --host <claude|codex|opencode|pi> [--hook] [--force]",
22836
22852
  purpose: "Install skills for your signed-in account\u2019s granted tools and an optional Claude Code session hook.",
@@ -22875,7 +22891,13 @@ var commands = {
22875
22891
  };
22876
22892
  function commandHelp(command) {
22877
22893
  const entry = commands[command];
22878
- const flags = [command === "falcon-axi" || command === "vectra-axi" || command === "ldap-axi" ? "--help before the tool name (default false)" : "--help (default false)", ...entry.flags];
22894
+ const flags = [
22895
+ command === "falcon-axi" || command === "vectra-axi" || command === "ldap-axi" ? "--help before the tool name (default false)" : "--help (default false)",
22896
+ ...entry.flags,
22897
+ "--managed-session before the command (default false; authenticated calls only; session JSON on inherited pipe fd 3, rotations as JSON lines on private pipe fd 4; no session file)",
22898
+ "Managed input: {serverUrl, accessToken, refreshToken?}; close fd 3 after JSON (64 KiB maximum). Output: {serverUrl, status, accessToken, refreshToken, accessExpiresAt, familyExpiresAt} per rotation, before retry, including on later failure.",
22899
+ "Managed exits: 0 success; 2 invalid JSON/descriptors or refused login/passwd/logout/setup; 1 authentication/transport failure; tool exit codes pass through. Only TOKEN_EXPIRED permits one refresh/retry; without refreshToken or on refusal, return control to the caller."
22900
+ ];
22879
22901
  return `usage: ${entry.usage}
22880
22902
  purpose: ${entry.purpose}
22881
22903
  flags[${flags.length}]:
@@ -33710,25 +33732,25 @@ function rewriteDiscoveryHelp(help, tool) {
33710
33732
  examples: help.examples.map((example) => hostedHelp(example, tool, flags, true))
33711
33733
  });
33712
33734
  }
33713
- function rewriteSuggestions(output2, tool, truncated = false) {
33714
- if (truncated) return output2;
33735
+ function rewriteSuggestions(output3, tool, truncated = false) {
33736
+ if (truncated) return output3;
33715
33737
  const flags = managedFlags[tool];
33716
- if (!flags) return output2;
33738
+ if (!flags) return output3;
33717
33739
  const executable = new RegExp(`^(?:artium-axi |npx -y @knowttl/)?${tool}(?=\\s|$)`);
33718
- if (executable.test(output2.replace(/^[ \t]*(?:(?:Command|Native operation|Examples): )?/, "")) || tool === "ldap-axi" && ldapHelp.test(output2)) return hostedHelp(output2, tool, flags, true);
33740
+ if (executable.test(output3.replace(/^[ \t]*(?:(?:Command|Native operation|Examples): )?/, "")) || tool === "ldap-axi" && ldapHelp.test(output3)) return hostedHelp(output3, tool, flags, true);
33719
33741
  let spans;
33720
33742
  let unknownOutcome = false;
33721
33743
  try {
33722
- const json2 = output2.trimStart().startsWith("{");
33723
- let parsed = json2 ? jsonHelp(output2) : toonHelp(output2);
33744
+ const json2 = output3.trimStart().startsWith("{");
33745
+ let parsed = json2 ? jsonHelp(output3) : toonHelp(output3);
33724
33746
  if (tool === "ldap-axi" && parsed.document.description === "Read Active Directory and LDAPv3 directories safely with ldap-axi (read-only)" && Array.isArray(parsed.document.commands) && parsed.spans.length) {
33725
33747
  const home = {
33726
33748
  ...parsed.document,
33727
33749
  commands: parsed.document.commands.filter((command) => ldapCommands.includes(command)),
33728
33750
  help: [...parsed.spans.map((span) => span.value).filter((value) => !/^(?:Authentication auto selects|Set LDAP_AXI_URL)/.test(value) && value !== ldapCredentialGuidance), ldapCredentialGuidance]
33729
33751
  };
33730
- output2 = (json2 ? JSON.stringify(home) : encode3(home)) + "\n";
33731
- parsed = json2 ? jsonHelp(output2) : toonHelp(output2);
33752
+ output3 = (json2 ? JSON.stringify(home) : encode3(home)) + "\n";
33753
+ parsed = json2 ? jsonHelp(output3) : toonHelp(output3);
33732
33754
  }
33733
33755
  const { spans: suggestions, document } = parsed;
33734
33756
  spans = suggestions;
@@ -33736,15 +33758,15 @@ function rewriteSuggestions(output2, tool, truncated = false) {
33736
33758
  unknownOutcome = document?.code === "UNKNOWN_OUTCOME" || document?.error?.code === "UNKNOWN_OUTCOME";
33737
33759
  }
33738
33760
  } catch {
33739
- return output2;
33761
+ return output3;
33740
33762
  }
33741
33763
  for (const span of [...spans].reverse()) {
33742
33764
  const value = hostedHelp(span.value, tool, flags, /^[ \t]*(?:Command|Native operation|Examples):/m.test(span.value));
33743
33765
  if (value === span.value) continue;
33744
- output2 = output2.slice(0, span.start) + (span.quoted || tool === "ldap-axi" && /[,\r\n]/.test(value) ? JSON.stringify(value) : value) + output2.slice(span.start + span.raw.length);
33766
+ output3 = output3.slice(0, span.start) + (span.quoted || tool === "ldap-axi" && /[,\r\n]/.test(value) ? JSON.stringify(value) : value) + output3.slice(span.start + span.raw.length);
33745
33767
  }
33746
- spans = (output2.trimStart().startsWith("{") ? jsonHelp(output2) : toonHelp(output2)).spans;
33747
- if (spans.some((span) => /\\[`'"]/.test(span.value))) return output2;
33768
+ spans = (output3.trimStart().startsWith("{") ? jsonHelp(output3) : toonHelp(output3)).spans;
33769
+ if (spans.some((span) => /\\[`'"]/.test(span.value))) return output3;
33748
33770
  const insertions = /* @__PURE__ */ new Map();
33749
33771
  const removals = /* @__PURE__ */ new Map();
33750
33772
  for (const span of spans) {
@@ -33778,28 +33800,28 @@ function rewriteSuggestions(output2, tool, truncated = false) {
33778
33800
  if (!quote2 && /^(?:artium-axi |npx -y @knowttl\/)?\S+-axi(?:\s|$)/.test(command)) commandRanges.push([match.index, match.index + match[0].length]);
33779
33801
  const wrapped = command.startsWith(`artium-axi ${tool}`);
33780
33802
  if (!executable.test(quote2 ? command.split(quote2)[0] : command)) continue;
33781
- if (quote2 && !command.includes(quote2)) return output2;
33803
+ if (quote2 && !command.includes(quote2)) return output3;
33782
33804
  const start = match.index + match[0].length - command.length;
33783
33805
  commandRanges.push([match.index, quote2 ? start + command.indexOf(quote2) + 1 : match.index + match[0].length]);
33784
33806
  commandEdit2(start, quote2 ? start + command.indexOf(quote2) : start + command.length, wrapped);
33785
33807
  }
33786
33808
  for (const match of span.value.matchAll(/(?<![^\s(])([`'"])([^\n]*?)(\1|(?=\n|$))/g)) {
33787
33809
  if (commandRanges.some(([start, end]) => match.index >= start && match.index < end)) continue;
33788
- if (!/^(?:artium-axi |npx -y @knowttl\/)?[^\s`'"]+-axi(?:\s|$)/.test(match[2])) return output2;
33810
+ if (!/^(?:artium-axi |npx -y @knowttl\/)?[^\s`'"]+-axi(?:\s|$)/.test(match[2])) return output3;
33789
33811
  const preceding = span.value.slice(0, match.index);
33790
33812
  if (/(?:^|\n)[ \t]*Valid flags for $/.test(preceding) && match[1] === "`" && match[3] === "`") continue;
33791
- if (!/(?:^|\n|\S[`'"])[ \t]*(?:(?:Run|Use|run|use|Verify with|If you're unsure, run|Check current state before trying again:|or(?:, if you're unsure, run)?)[ \t]+)?$/.test(preceding)) return output2;
33813
+ if (!/(?:^|\n|\S[`'"])[ \t]*(?:(?:Run|Use|run|use|Verify with|If you're unsure, run|Check current state before trying again:|or(?:, if you're unsure, run)?)[ \t]+)?$/.test(preceding)) return output3;
33792
33814
  const wrapped = match[2].startsWith(`artium-axi ${tool}`);
33793
33815
  if (executable.test(match[2])) {
33794
- if (!match[3]) return output2;
33816
+ if (!match[3]) return output3;
33795
33817
  commandEdit2(match.index + 1, match.index + 1 + match[2].length, wrapped);
33796
33818
  }
33797
33819
  }
33798
33820
  }
33799
33821
  for (const offset of [.../* @__PURE__ */ new Set([...insertions.keys(), ...removals.keys()])].sort((a, b) => b - a)) {
33800
- output2 = output2.slice(0, offset) + (insertions.get(offset) ?? "") + output2.slice(removals.get(offset) ?? offset);
33822
+ output3 = output3.slice(0, offset) + (insertions.get(offset) ?? "") + output3.slice(removals.get(offset) ?? offset);
33801
33823
  }
33802
- return output2;
33824
+ return output3;
33803
33825
  }
33804
33826
  function managedFlagRanges(command, tool) {
33805
33827
  const tokens = [...command.matchAll(/(?:[^\s;'"`]+|'[^']*'|"(?:\\.|[^"\\])*")+|;/g)];
@@ -33845,10 +33867,10 @@ function hostedHelp(help, tool, flags, commands2) {
33845
33867
  (match, separator) => separator ? `; artium-axi ${tool}` : match
33846
33868
  )).replace(new RegExp("`(?:npx -y @knowttl/)?" + tool + "(?=\\s|`)([^\\r\\n`]*`)", "g"), "`artium-axi " + tool + "$1").replace(new RegExp(`^([ ]*cat [^\\r\\n|]+ \\| )(?:npx -y @knowttl/)?${tool}(?=\\s|$)`, "gm"), `$1artium-axi ${tool}`).replace(new RegExp(`\\buse ${tool} entry search\\b`, "g"), `use artium-axi ${tool} entry search`);
33847
33869
  }
33848
- function jsonHelp(output2) {
33849
- const parsed = JSON.parse(output2);
33870
+ function jsonHelp(output3) {
33871
+ const parsed = JSON.parse(output3);
33850
33872
  if (typeof parsed.help !== "string" && !(Array.isArray(parsed.help) && parsed.help.every((item) => typeof item === "string"))) return { spans: [], document: parsed };
33851
- const tokens = [...output2.matchAll(/"(?:\\[\s\S]|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}\[\]:,]/g)];
33873
+ const tokens = [...output3.matchAll(/"(?:\\[\s\S]|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}\[\]:,]/g)];
33852
33874
  const spans = [];
33853
33875
  let index = 0;
33854
33876
  let helpFields = 0;
@@ -33876,8 +33898,8 @@ function jsonHelp(output2) {
33876
33898
  value(false, true);
33877
33899
  return { spans: helpFields === 1 ? spans : [], document: parsed };
33878
33900
  }
33879
- function toonHelp(output2) {
33880
- const lines = [...output2.matchAll(/[^\r\n]*(?:\r\n|\n|$)/g)].filter((line) => line[0]);
33901
+ function toonHelp(output3) {
33902
+ const lines = [...output3.matchAll(/[^\r\n]*(?:\r\n|\n|$)/g)].filter((line) => line[0]);
33881
33903
  const spans = [];
33882
33904
  const validation = lines.map((line) => line[0].replace(/\r?\n$/, ""));
33883
33905
  let helpFields = 0;
@@ -33958,8 +33980,8 @@ accessExpiresAt: ${user.accessExpiresAt}
33958
33980
  }
33959
33981
  function toolsView(tools) {
33960
33982
  if (!tools.length) return "tools: 0 available\nreason: No enabled runtime tool is available through your assigned roles.\ncontact: Ask an administrator to check your role assignments and enabled tools.\nnext[1]:\n - artium-axi whoami\n";
33961
- return `tools[${tools.length}]{name,status,version}:
33962
- ${tools.map((tool) => ` ${tool.name},${tool.status},${JSON.stringify(tool.version)}`).join("\n")}
33983
+ return `tools[${tools.length}]{name,status,version,readOnly}:
33984
+ ${tools.map((tool) => ` ${tool.name},${tool.status},${JSON.stringify(tool.version)},${tool.readOnly ?? true}`).join("\n")}
33963
33985
  next[2]:
33964
33986
  - artium-axi tools show ${tools[0].name}
33965
33987
  - artium-axi ${tools[0].name} ${toolManifests[tools[0].name].discovery.example.join(" ")}
@@ -33973,6 +33995,7 @@ function toolView(tool) {
33973
33995
  return `tool: ${tool.name}
33974
33996
  version: ${JSON.stringify(tool.version)}
33975
33997
  status: ${tool.status}
33998
+ readOnly: ${tool.readOnly ?? true}
33976
33999
  commands[${commands2.length}]: ${commands2.join(",")}
33977
34000
  example: artium-axi ${tool.name} ${manifest.discovery.example.join(" ")}
33978
34001
  upstreamHelp: ${JSON.stringify(help)}
@@ -34055,15 +34078,15 @@ function claudeSettings(path, executable) {
34055
34078
  if (!Array.isArray(sessions) || sessions.some((entry) => !object2(entry) || !Array.isArray(entry.hooks) || entry.hooks.some((hook) => !object2(hook)))) {
34056
34079
  throw new ArtiumError("INVALID_INPUT", "Claude SessionStart hooks have an unsupported shape; no installation was written.");
34057
34080
  }
34058
- const managed = { type: "command", command: process.execPath, args: [executable, "tools"], statusMessage: hookName };
34081
+ const managed2 = { type: "command", command: process.execPath, args: [executable, "tools"], statusMessage: hookName };
34059
34082
  let found = false;
34060
34083
  const merged = sessions.map((entry) => ({ ...entry, hooks: entry.hooks.flatMap((hook) => {
34061
34084
  if (hook.statusMessage !== hookName) return [hook];
34062
34085
  if (found) return [];
34063
34086
  found = true;
34064
- return [managed];
34087
+ return [managed2];
34065
34088
  }) }));
34066
- if (!found) merged.push({ hooks: [managed] });
34089
+ if (!found) merged.push({ hooks: [managed2] });
34067
34090
  settings.hooks = { ...hooks, SessionStart: merged };
34068
34091
  return JSON.stringify(settings, null, 2) + "\n";
34069
34092
  }
@@ -34224,24 +34247,199 @@ next: artium-axi tools
34224
34247
  throw new ArtiumError("DEVICE_CODE_EXPIRED");
34225
34248
  }
34226
34249
 
34250
+ // src/cli/managed-session.ts
34251
+ import { fstatSync, writeSync } from "node:fs";
34252
+ import { Socket } from "node:net";
34253
+
34254
+ // src/server/runner/scan.ts
34255
+ function prefixLengths(bytes) {
34256
+ const lengths = new Uint32Array(bytes.length);
34257
+ let length = 0;
34258
+ for (let index = 1; index < bytes.length; index++) {
34259
+ while (length > 0 && bytes[index] !== bytes[length]) length = lengths[length - 1];
34260
+ if (bytes[index] === bytes[length]) length++;
34261
+ lengths[index] = length;
34262
+ }
34263
+ return lengths;
34264
+ }
34265
+ var SecretScanner = class {
34266
+ #patterns;
34267
+ #positions;
34268
+ #matched = false;
34269
+ constructor(secrets) {
34270
+ const forms = /* @__PURE__ */ new Set();
34271
+ for (const secret of secrets) {
34272
+ if (secret.length === 0) throw new Error("Secret values must be nonempty");
34273
+ const bytes = Buffer.from(secret, "utf8");
34274
+ forms.add(secret);
34275
+ forms.add(bytes.toString("base64"));
34276
+ forms.add(bytes.toString("base64url"));
34277
+ forms.add(encodeURIComponent(secret));
34278
+ forms.add(JSON.stringify(secret).slice(1, -1));
34279
+ }
34280
+ this.#patterns = [...forms].map((form) => {
34281
+ const bytes = Buffer.from(form, "utf8");
34282
+ return { bytes, prefixes: prefixLengths(bytes) };
34283
+ });
34284
+ this.#positions = {
34285
+ stdout: new Uint32Array(this.#patterns.length),
34286
+ stderr: new Uint32Array(this.#patterns.length)
34287
+ };
34288
+ }
34289
+ scan(stream, chunk) {
34290
+ if (this.#matched) return true;
34291
+ const positions = this.#positions[stream];
34292
+ for (let index = 0; index < this.#patterns.length; index++) {
34293
+ const { bytes, prefixes } = this.#patterns[index];
34294
+ let position = positions[index];
34295
+ for (const byte of chunk) {
34296
+ while (position > 0 && byte !== bytes[position]) position = prefixes[position - 1];
34297
+ if (byte === bytes[position]) position++;
34298
+ if (position === bytes.length) {
34299
+ this.#matched = true;
34300
+ return true;
34301
+ }
34302
+ }
34303
+ positions[index] = position;
34304
+ }
34305
+ return false;
34306
+ }
34307
+ };
34308
+
34309
+ // src/cli/managed-session.ts
34310
+ var sessionSchema = external_exports.strictObject({
34311
+ serverUrl: external_exports.string().url(),
34312
+ accessToken: external_exports.string().min(1),
34313
+ refreshToken: external_exports.string().min(1).optional(),
34314
+ status: external_exports.literal("authenticated").optional(),
34315
+ accessExpiresAt: external_exports.number().int().optional(),
34316
+ familyExpiresAt: external_exports.number().int().optional()
34317
+ });
34318
+ var ManagedSession = class _ManagedSession {
34319
+ constructor(session, channel) {
34320
+ this.session = session;
34321
+ this.channel = channel;
34322
+ this.secrets = [session.accessToken, ...session.refreshToken ? [session.refreshToken] : []];
34323
+ }
34324
+ session;
34325
+ channel;
34326
+ secrets;
34327
+ output = { stdout: "", stderr: "" };
34328
+ static async read() {
34329
+ try {
34330
+ for (const fd of [3, 4]) {
34331
+ const stat = fstatSync(fd);
34332
+ if (!stat.isFIFO() && !stat.isSocket()) throw new Error();
34333
+ }
34334
+ writeSync(4, Buffer.alloc(0));
34335
+ const input2 = new Socket({ fd: 3, readable: true, writable: false });
34336
+ const chunks = [];
34337
+ let bytes = 0;
34338
+ for await (const chunk of input2) {
34339
+ bytes += chunk.length;
34340
+ if (bytes > 64 * 1024) {
34341
+ input2.destroy();
34342
+ throw new Error();
34343
+ }
34344
+ chunks.push(chunk);
34345
+ }
34346
+ const session = sessionSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
34347
+ const channel = new Socket({ fd: 4, readable: false, writable: true });
34348
+ channel.on("error", () => {
34349
+ });
34350
+ return new _ManagedSession(session, channel);
34351
+ } catch {
34352
+ throw new ArtiumError(
34353
+ "INVALID_INPUT",
34354
+ "Managed session input or private descriptors are invalid.",
34355
+ "Supply session JSON on inherited pipe fd 3 and a writable private pipe on fd 4; close fd 3 after sending JSON."
34356
+ );
34357
+ }
34358
+ }
34359
+ checkServer(serverUrl) {
34360
+ if (this.session.serverUrl.replace(/\/$/, "") !== serverUrl) throw new ArtiumError("SESSION_SERVER_MISMATCH");
34361
+ }
34362
+ async authenticated(request, refresh) {
34363
+ try {
34364
+ return await request(this.session.accessToken);
34365
+ } catch (error62) {
34366
+ if (!(error62 instanceof ArtiumError) || error62.code !== "TOKEN_EXPIRED" || !this.session.refreshToken) throw error62;
34367
+ }
34368
+ const parsed = loginResponseSchema.safeParse(await refresh(this.session.refreshToken));
34369
+ if (!parsed.success || parsed.data.status !== "authenticated" || !parsed.data.accessToken || !parsed.data.refreshToken) {
34370
+ throw new ArtiumError("INVALID_RESPONSE");
34371
+ }
34372
+ this.session = { serverUrl: this.session.serverUrl, ...parsed.data };
34373
+ this.secrets.push(parsed.data.accessToken, parsed.data.refreshToken);
34374
+ try {
34375
+ await new Promise((resolve3, reject) => {
34376
+ this.channel.write(`${JSON.stringify(this.session)}
34377
+ `, (error62) => error62 ? reject(error62) : resolve3());
34378
+ });
34379
+ } catch {
34380
+ throw new ArtiumError(
34381
+ "INVALID_INPUT",
34382
+ "Managed session rotation could not be returned.",
34383
+ "Recover the session through the caller; do not retry using the old refresh token."
34384
+ );
34385
+ }
34386
+ return request(this.session.accessToken);
34387
+ }
34388
+ write(stream, text2) {
34389
+ this.output[stream] += text2;
34390
+ }
34391
+ flush() {
34392
+ this.channel.destroy();
34393
+ const scanner = new SecretScanner(this.secrets);
34394
+ if (scanner.scan("stdout", Buffer.from(this.output.stdout)) || scanner.scan("stderr", Buffer.from(this.output.stderr))) {
34395
+ throw new ArtiumError("SECRET_IN_OUTPUT");
34396
+ }
34397
+ process.stdout.write(this.output.stdout);
34398
+ process.stderr.write(this.output.stderr);
34399
+ }
34400
+ };
34401
+
34227
34402
  // src/cli/main.ts
34403
+ var managed;
34404
+ function output2(stream, text2) {
34405
+ if (managed) managed.write(stream, text2);
34406
+ else process[stream].write(text2);
34407
+ }
34228
34408
  async function main(args) {
34409
+ const managedMode = args[0] === "--managed-session";
34410
+ if (managedMode) {
34411
+ args = args.slice(1);
34412
+ if (["login", "passwd", "logout", "setup"].includes(args[0])) {
34413
+ throw new ArtiumError(
34414
+ "INVALID_INPUT",
34415
+ "This command is unavailable in managed session mode.",
34416
+ "The caller must acquire and manage tokens; use managed mode for authenticated calls."
34417
+ );
34418
+ }
34419
+ managed = await ManagedSession.read();
34420
+ }
34421
+ if (args.length === 1 && args[0] === "--version") {
34422
+ output2("stdout", `${cliVersion}
34423
+ `);
34424
+ return;
34425
+ }
34229
34426
  const prefixHelp = args[0] === "--help";
34230
34427
  if (prefixHelp) args = args.slice(1);
34231
34428
  const command = args[0];
34232
34429
  const help = helpCommand(prefixHelp ? [...args, "--help"] : args, prefixHelp);
34233
34430
  if (prefixHelp && !help) throw new ArtiumError("INVALID_INPUT", void 0, "Use artium-axi --help for commands.");
34234
34431
  if (help) {
34235
- process.stdout.write(commandHelp(help));
34432
+ output2("stdout", commandHelp(help));
34236
34433
  return;
34237
34434
  }
34238
34435
  if (command === "setup") {
34239
- process.stdout.write(await setup(args.slice(1)));
34436
+ output2("stdout", await setup(args.slice(1)));
34240
34437
  return;
34241
34438
  }
34242
34439
  if (command === "doctor") {
34243
34440
  if (args.length !== 1) throw new ArtiumError("INVALID_INPUT", void 0, "Use artium-axi doctor.");
34244
- await doctor();
34441
+ if (managed) await doctor(() => new Client(process.env, managed), (text2) => output2("stdout", text2));
34442
+ else await doctor();
34245
34443
  return;
34246
34444
  }
34247
34445
  if (command === "login" || command === "passwd") {
@@ -34276,14 +34474,14 @@ async function main(args) {
34276
34474
  return parsed.data;
34277
34475
  });
34278
34476
  if (session.status === "password-change-required") throw new ArtiumError("PASSWORD_CHANGE_REQUIRED");
34279
- process.stdout.write("status: authenticated\nnext: artium-axi tools\n");
34477
+ output2("stdout", "status: authenticated\nnext: artium-axi tools\n");
34280
34478
  } else {
34281
34479
  await client2.changePassword(async (session) => {
34282
34480
  const newPassword = await readPassword(stdin);
34283
34481
  const currentPassword = session.status === "authenticated" ? await readPassword(false, "ARTIUM_AXI_CURRENT_PASSWORD") : void 0;
34284
34482
  return { newPassword, ...currentPassword === void 0 ? {} : { currentPassword } };
34285
34483
  });
34286
- process.stdout.write("status: password-changed\nnext: artium-axi login --username <username> --password-stdin\n");
34484
+ output2("stdout", "status: password-changed\nnext: artium-axi login --username <username> --password-stdin\n");
34287
34485
  }
34288
34486
  return;
34289
34487
  }
@@ -34308,10 +34506,10 @@ async function main(args) {
34308
34506
  return false;
34309
34507
  }
34310
34508
  });
34311
- process.stdout.write(`status: logged-out
34509
+ output2("stdout", `status: logged-out
34312
34510
  remoteRevocation: ${verified ? "verified" : "unverified"}
34313
34511
  `);
34314
- if (!verified) process.stdout.write("next: Remote revocation is unverified. The local tokens were removed.\n");
34512
+ if (!verified) output2("stdout", "next: Remote revocation is unverified. The local tokens were removed.\n");
34315
34513
  return;
34316
34514
  }
34317
34515
  const home = args.length === 0;
@@ -34322,25 +34520,25 @@ remoteRevocation: ${verified ? "verified" : "unverified"}
34322
34520
  throw new ArtiumError("INVALID_INPUT", "Unknown command, argument or flag.", "Use artium-axi --help for commands or artium-axi <command> --help for valid flags.");
34323
34521
  }
34324
34522
  const input2 = home || listing || whoami ? void 0 : await toolInput(command, args.slice(1));
34325
- const client = new Client();
34326
- const homeSession = home ? client.sessions.read() : void 0;
34327
- if (home && !homeSession) throw new ArtiumError("TOKEN_INVALID", void 0, "Run artium-axi login --username <username> --password-stdin.");
34523
+ const client = new Client(process.env, managed);
34524
+ const homeSession = home && !managed ? client.sessions.read() : void 0;
34525
+ if (home && !managed && !homeSession) throw new ArtiumError("TOKEN_INVALID", void 0, "Run artium-axi login --username <username> --password-stdin.");
34328
34526
  const result = await client.authenticated(home || whoami ? "/v1/me" : `/v1/tools${listing ? "" : `/${command}/run`}`, home || listing || whoami ? "GET" : "POST", input2, homeSession);
34329
34527
  if (home || whoami) {
34330
34528
  const parsed = meResponseSchema.safeParse(result);
34331
34529
  if (!parsed.success) throw new ArtiumError("INVALID_RESPONSE");
34332
- let output2 = identityView(parsed.data, client.serverUrl);
34530
+ let view = identityView(parsed.data, client.serverUrl);
34333
34531
  if (home) {
34334
34532
  const tools = toolsResponseSchema.safeParse(await client.authenticated("/v1/tools", "GET", void 0, homeSession));
34335
34533
  if (!tools.success) throw new ArtiumError("INVALID_RESPONSE");
34336
34534
  const executable = resolve2(process.argv[1]);
34337
34535
  const local = relative(homedir3(), executable);
34338
34536
  const bin = local && !isAbsolute(local) && !local.startsWith(`..${sep}`) && local !== ".." ? `~${sep}${local}` : executable;
34339
- output2 = `bin: ${JSON.stringify(bin)}
34537
+ view = `bin: ${JSON.stringify(bin)}
34340
34538
  description: Run managed upstream tools through one local sign-in.
34341
- ${output2}${toolsView(tools.data.tools)}`;
34539
+ ${view}${toolsView(tools.data.tools)}`;
34342
34540
  }
34343
- process.stdout.write(output2);
34541
+ output2("stdout", view);
34344
34542
  } else if (listing) {
34345
34543
  const parsed = toolsResponseSchema.safeParse(result);
34346
34544
  if (!parsed.success) throw new ArtiumError("INVALID_RESPONSE");
@@ -34348,23 +34546,30 @@ ${output2}${toolsView(tools.data.tools)}`;
34348
34546
  if (detail) {
34349
34547
  const tool = tools.find((tool2) => tool2.name === args[2]);
34350
34548
  if (!tool) throw new ArtiumError("TOOL_NOT_GRANTED", "The tool is not available to your account.", "Ask an administrator to check your role assignments and enabled tools.");
34351
- process.stdout.write(toolView(tool));
34352
- } else process.stdout.write(toolsView(tools));
34549
+ output2("stdout", toolView(tool));
34550
+ } else output2("stdout", toolsView(tools));
34353
34551
  } else {
34354
34552
  const parsed = runSuccessSchema.safeParse(result);
34355
34553
  if (!parsed.success || parsed.data.source !== command) throw new ArtiumError("INVALID_RESPONSE");
34356
34554
  const { stdout, stderr, exitCode, truncated } = parsed.data;
34357
- if (truncated.stdout || truncated.stderr) process.stderr.write(`artium-axi: output truncated (${["stdout", "stderr"].filter((stream) => truncated[stream]).join(", ")}).
34555
+ if (truncated.stdout || truncated.stderr) output2("stderr", `artium-axi: output truncated (${["stdout", "stderr"].filter((stream) => truncated[stream]).join(", ")}).
34358
34556
  `);
34359
- process.stdout.write(rewriteSuggestions(stdout, command, truncated.stdout));
34360
- process.stderr.write(rewriteSuggestions(stderr, command, truncated.stderr));
34557
+ output2("stdout", rewriteSuggestions(stdout, command, truncated.stdout));
34558
+ output2("stderr", rewriteSuggestions(stderr, command, truncated.stderr));
34361
34559
  process.exitCode = exitCode;
34362
34560
  }
34363
34561
  }
34364
34562
  function printError(response) {
34365
- process.stdout.write(`error:
34563
+ output2("stdout", `error:
34366
34564
  ${Object.entries(response.error).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`).join("\n")}
34367
34565
  `);
34368
34566
  process.exitCode = errors[response.error.code].exitCode;
34369
34567
  }
34370
- main(process.argv.slice(2)).catch((error62) => printError(errorResponse(error62 instanceof ArtiumError ? error62 : new ArtiumError("INTERNAL_ERROR"), randomUUID3())));
34568
+ main(process.argv.slice(2)).catch((error62) => printError(errorResponse(error62 instanceof ArtiumError ? error62 : new ArtiumError("INTERNAL_ERROR"), randomUUID3()))).then(() => {
34569
+ try {
34570
+ managed?.flush();
34571
+ } catch {
34572
+ managed = void 0;
34573
+ printError(errorResponse(new ArtiumError("SECRET_IN_OUTPUT"), randomUUID3()));
34574
+ }
34575
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowttl/artium-axi",
3
- "version": "0.0.0",
3
+ "version": "0.0.3",
4
4
  "description": "Client CLI and embedded skill for the Artium managed tools server.",
5
5
  "license": "MIT",
6
6
  "type": "module",