hirify-cli 0.5.0 → 0.5.1

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
@@ -121,6 +121,28 @@ Explicitly safe reads get bounded retries for transient network errors and 429/5
121
121
  Feedback accepts `--idempotency-key` when the server advertises caller-keyed deduplication;
122
122
  keep the same key for a deliberate retry. The CLI prints a generated request reference otherwise.
123
123
 
124
+ ## Codex and other sandboxed agents
125
+
126
+ Sign in once from your normal terminal. A local agent can reuse that sign-in when it has access
127
+ to the same configuration directory. It does not need a separate API key.
128
+ Reading an unexpired session does not create files or change permissions. Renewing an OAuth
129
+ session requires write access so the rotated token can be saved safely.
130
+
131
+ A Windows Codex sandbox can run with a restricted token or a different Windows account. Success
132
+ in `cmd` therefore does not prove the sandbox can access the saved sign-in. Keep credentials in
133
+ their normal private directory. If a command is denied, let Codex request approval for that specific
134
+ command using its normal permission mechanism. Do not disable the whole sandbox, grant `Everyone`
135
+ access, copy `auth.json` into the project, or paste a token into chat.
136
+ Read permission alone will not support token renewal. If approval is unavailable, report the
137
+ restriction instead of repeatedly running login. WSL and containers have separate home directories;
138
+ a Windows login is not automatically shared with them.
139
+
140
+ Compare `hirify version`, `hirify doctor`, and `hirify auth status --json --error-format=json`
141
+ inside the agent and in the terminal. These commands do not print tokens. `storage_unreadable`
142
+ means reading was denied or failed; `storage_unavailable`, `storage_lock_failed`, and
143
+ `storage_write_failed` identify preparation, locking, or saving failures. An access failure does
144
+ not by itself mean the sign-in expired. See [Codex Windows sandbox documentation](https://learn.chatgpt.com/docs/windows/windows-sandbox).
145
+
124
146
  ## Configuration and diagnostics
125
147
 
126
148
  Access is stored in `auth.json` under `$XDG_CONFIG_HOME/hirify` when XDG_CONFIG_HOME is absolute,
package/bin/lib/auth.js CHANGED
@@ -42,7 +42,6 @@ export function createAuth({ config, store, http, signal, output, event, secrets
42
42
  }
43
43
  async function access() {
44
44
  if (config.envKey) { secrets.add(config.envKey); return config.envKey }
45
- await store.secure()
46
45
  let current = remember(store.read({ allowPending: true }))
47
46
  if (!current) throw new CliError('auth_required', 'You are not signed in. Run hirify login. For CI or servers, set HIRIFY_KEY or use hirify auth --stdin.')
48
47
  if (current.kind === 'oauth' && (current.refresh_pending || (current.expires_at && current.expires_at <= Date.now() / 1000 + 60))) current = await refresh(current)
package/bin/lib/cli.js CHANGED
@@ -99,7 +99,7 @@ export function parse(argv) {
99
99
  }
100
100
  export function help(topic = '') {
101
101
  const entries = Object.entries(COMMANDS).filter(([name]) => !topic || name === topic || name.startsWith(topic + ' '))
102
- return `hirify - job search for AI agents\n\n${(entries.length ? entries : Object.entries(COMMANDS)).map(([name, spec]) => ` hirify ${(name + (ARGUMENTS[name] ? ' ' + ARGUMENTS[name] : '')).padEnd(35)} ${spec.description}${spec.flags?.length ? `\n ${spec.flags.map(f => '--' + f + (BOOLEAN_FLAGS.has(f) ? '' : ' <value>')).join(' ')}` : ''}`).join('\n')}\n\nGlobal options: --json, --fields a,b, --error-format json|text, --timeout <seconds>, --debug, --help.\nUse -- to end options; --name=value and --name value are supported.\nLists and search are free. Vacancy read, reveal and apply have separate allowances.\nCheck them with hirify account show. Apply sends a real application: ask the person first.\nHeadless/CI: HIRIFY_KEY or hirify auth --stdin. Manual login: --no-browser.\nSSH: forward the chosen --callback-port to the same loopback port on the CLI host.\nRules for your agent: npx skills add hirifyme/hirify-cli\n`
102
+ return `hirify - job search for AI agents\n\n${(entries.length ? entries : Object.entries(COMMANDS)).map(([name, spec]) => ` hirify ${(name + (ARGUMENTS[name] ? ' ' + ARGUMENTS[name] : '')).padEnd(35)} ${spec.description}${spec.flags?.length ? `\n ${spec.flags.map(f => '--' + f + (BOOLEAN_FLAGS.has(f) ? '' : ' <value>')).join(' ')}` : ''}`).join('\n')}\n\nGlobal options: --json, --fields a,b, --error-format json|text, --timeout <seconds>, --debug, --help.\nUse -- to end options; --name=value and --name value are supported.\nLists and search are free. Vacancy read, reveal and apply have separate allowances.\nCheck them with hirify account show. Apply sends a real application: ask the person first.\nLocal agents reuse saved sign-in when allowed. Token renewal needs write access.\nSandbox access denied: request permission for the command; do not copy tokens.\nHeadless/CI: HIRIFY_KEY or hirify auth --stdin. Manual login: --no-browser.\nSSH: forward the chosen --callback-port to the same loopback port on the CLI host.\nRules for your agent: npx skills add hirifyme/hirify-cli\n`
103
103
  }
104
104
  export function createOutput({ stdout = process.stdout, stderr = process.stderr, json = false, errorJSON = false, secrets = new Set() } = {}) {
105
105
  let closed = false
package/bin/lib/store.js CHANGED
@@ -7,6 +7,12 @@ import { CliError, aborted } from './errors.js'
7
7
  import { DEFAULT_API, serverURL } from './config.js'
8
8
  import { delay } from './http.js'
9
9
 
10
+ function storageError(code, operation, error) {
11
+ // Never include OS error messages: they may contain private paths or user input.
12
+ const reason = ['EACCES', 'EPERM', 'EROFS', 'ENOSPC', 'EDQUOT', 'ENOENT', 'ENOTDIR', 'ERR_ACCESS_DENIED'].includes(error?.code) ? ` (${error.code})` : ''
13
+ const access = operation === 'read' ? 'read access' : 'write access'
14
+ return new CliError(code, `Could not ${operation} the saved sign-in${reason}. This command needs ${access} to the Hirify configuration directory. If it runs in an agent sandbox, request permission for this command through the agent. Otherwise, check directory permissions and available disk space. Do not copy tokens into the project or chat.`)
15
+ }
10
16
  function stat(path) { try { return lstatSync(path) } catch (e) { if (e.code === 'ENOENT') return null; throw e } }
11
17
  function check(path, directory = false) {
12
18
  const s = stat(path)
@@ -28,13 +34,13 @@ export function createStore(config, { signal } = {}) {
28
34
  const file = join(config.dir, 'auth.json')
29
35
  const legacy = join(config.dir, 'key')
30
36
  let compromised = false
31
- let secured = false
32
37
  async function ensure() {
33
38
  try { mkdirSync(config.dir, { recursive: true, mode: 0o700 }); await makePrivate(config.dir, true, signal) }
34
- catch (e) { if (signal?.aborted) throw signal.reason; if (e instanceof CliError) throw e; throw new CliError('storage_unavailable', 'Could not create a private credential directory. Check its permissions.') }
39
+ catch (e) { if (signal?.aborted) throw signal.reason; if (e instanceof CliError) throw e; throw storageError('storage_unavailable', 'prepare', e) }
35
40
  }
36
41
  function readRaw() {
37
42
  try {
43
+ check(config.dir, true)
38
44
  const stateStat = check(file)
39
45
  if (stateStat) {
40
46
  const raw = readFileSync(file, 'utf8')
@@ -50,7 +56,7 @@ export function createStore(config, { signal } = {}) {
50
56
  if (key) return { kind: 'key', issuer: DEFAULT_API, access_token: key, generation: `legacy:${createHash('sha256').update(key).digest('hex')}` }
51
57
  }
52
58
  return { kind: 'signed-out', generation: 'absent' }
53
- } catch (e) { if (e instanceof CliError) throw e; throw new CliError('storage_unreadable', 'Could not read the saved sign-in. Check the credential directory permissions.') }
59
+ } catch (e) { if (e instanceof CliError) throw e; throw storageError('storage_unreadable', 'read', e) }
54
60
  }
55
61
  function read({ allowPending = false } = {}) {
56
62
  const state = readRaw()
@@ -76,7 +82,7 @@ export function createStore(config, { signal } = {}) {
76
82
  }
77
83
  if (process.platform !== 'win32') { const dir = openSync(config.dir, 'r'); try { fsyncSync(dir) } finally { closeSync(dir) } }
78
84
  if (check(legacy)) unlinkSync(legacy)
79
- } catch (e) { if (e instanceof CliError) throw e; throw new CliError('storage_write_failed', 'Could not save the sign-in safely. Check disk space and directory permissions. You may need to sign in again.') }
85
+ } catch (e) { if (e instanceof CliError) throw e; throw storageError('storage_write_failed', 'save', e) }
80
86
  finally { if (fd !== undefined) closeSync(fd); try { unlinkSync(temp) } catch {} }
81
87
  }
82
88
  async function locked(fn) {
@@ -87,7 +93,7 @@ export function createStore(config, { signal } = {}) {
87
93
  while (!release) {
88
94
  aborted(signal)
89
95
  try { release = await lockfile.lock(file, { realpath: false, lockfilePath: lockPath, stale: 120000, update: 10000, onCompromised: () => { compromised = true } }) }
90
- catch (e) { if (e.code !== 'ELOCKED') throw new CliError('storage_lock_failed', 'Could not lock the saved sign-in. Check directory permissions.'); await delay(40 + Math.random() * 80, signal) }
96
+ catch (e) { if (e.code !== 'ELOCKED') throw storageError('storage_lock_failed', 'lock', e); await delay(40 + Math.random() * 80, signal) }
91
97
  }
92
98
  try { aborted(signal); return await fn() } finally { await release().catch(() => {}) }
93
99
  }
@@ -113,6 +119,5 @@ export function createStore(config, { signal } = {}) {
113
119
  return state.generation
114
120
  })
115
121
  }
116
- async function secure() { if (secured) return; await ensure(); for (const path of [file, legacy]) if (check(path)) await makePrivate(path, false, signal); secured = true }
117
- return { file, read, readRaw, write, locked, commit, logout, beginLogin, secure }
122
+ return { file, read, readRaw, write, locked, commit, logout, beginLogin }
118
123
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "hirify-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "hirify-cli",
9
- "version": "0.5.0",
9
+ "version": "0.5.1",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "cross-spawn": "7.0.6",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hirify-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Hirify CLI for AI agents: browser sign-in, job feeds, search, reading a vacancy in full, contacts to apply to, and applying on Hirify.",
5
5
  "bin": {
6
6
  "hirify": "bin/hirify.js"
@@ -10,7 +10,8 @@
10
10
  "build": "node scripts/build.mjs",
11
11
  "test:package": "node scripts/verify-package.mjs",
12
12
  "prepublishOnly": "node scripts/prepublish-check.mjs",
13
- "test:install": "node scripts/install-update-smoke.mjs"
13
+ "test:install": "node scripts/install-update-smoke.mjs",
14
+ "test:codex": "node scripts/codex-sandbox-smoke.mjs"
14
15
  },
15
16
  "type": "module",
16
17
  "dependencies": {
@@ -17,8 +17,9 @@ for reproducible work use an exact version and `HIRIFY_NO_AUTO_UPDATE=1`.
17
17
 
18
18
  A person runs `hirify login` and confirms the printed link. The CLI tries a browser only when
19
19
  appropriate. Do not run login for them. `--no-browser` is manual callback login, not device flow.
20
- SSH needs `--callback-port` with a matching SSH local port forward. For agents, CI and containers,
21
- use `HIRIFY_KEY` or pipe a key to `hirify auth --stdin`; never put a key in a command, log or chat.
20
+ SSH needs `--callback-port` with a matching SSH local port forward. Local agents reuse the person's
21
+ saved sign-in when permitted to access the same configuration directory. CI and separate containers
22
+ can use `HIRIFY_KEY` or `hirify auth --stdin`; never put a key in a command, log or chat.
22
23
  `hirify auth status --json` identifies the local source. Environment keys override saved access;
23
24
  logout is local and does not unset them. Use `hirify login --force` to replace expired access.
24
25
 
@@ -107,7 +108,13 @@ errors; do not combine it with debug if expecting one object. Exit 0 means succe
107
108
  2 unsupported server manifest, 130/143 interruption. Read the stable error code and message.
108
109
 
109
110
  - `interaction_required`: ask the person to sign in or configure a key; do not wait on browser login.
110
- - Authentication failure: inspect `auth status`, then ask for `login --force`. Never expose a token.
111
+ - Storage access failure: compare `doctor`, `version`, and `auth status` with the normal terminal.
112
+ Request permission for the specific command through the agent's approval mechanism. Reading a
113
+ current token needs read access; renewal needs write access. Do not treat storage errors as an
114
+ expired login, copy credentials into the workspace, broaden Windows ACLs, or disable the sandbox.
115
+ Codex on Windows may use a different account; WSL/containers do not share the Windows home.
116
+ - Authentication failure: inspect `auth status`; ask for `login --force` only when renewal requires
117
+ a new sign-in. Never expose a token.
111
118
  - 403: permission or plan restriction. 429: pace or a named allowance; inspect `account show`.
112
119
  - Network/TLS/proxy failure: fix connectivity or trust configuration; never disable TLS verification.
113
120
  - `outcome_unknown`: a metered action or mutation may have succeeded. Check before repeating;