atris 3.62.0 → 3.64.0
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/features/README.md +8 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/commerce/SKILL.md +120 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +22 -2
- package/cli/atris_code.py +25 -1
- package/commands/auth.js +79 -10
- package/commands/autoland.js +11 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +3 -3
- package/commands/buy.js +242 -0
- package/commands/deck.js +2 -2
- package/commands/engine.js +122 -13
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/member.js +82 -15
- package/commands/mission.js +4 -1
- package/commands/pack-craft.js +2 -2
- package/commands/publish.js +2 -2
- package/commands/radar.js +3 -2
- package/commands/router.js +2 -2
- package/commands/task.js +91 -2
- package/commands/transactions.js +115 -0
- package/commands/truth.js +2 -2
- package/commands/x-search.js +81 -10
- package/commands/youtube.js +24 -1
- package/lib/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -0
- package/lib/chat-log-scan.js +2 -1
- package/lib/ci-runner.js +3 -2
- package/lib/codex-flight.js +2 -1
- package/lib/commerce.js +90 -0
- package/lib/deck-history.js +2 -2
- package/lib/deck-review.js +3 -3
- package/lib/design-api.js +2 -2
- package/lib/engine-registry.js +102 -18
- package/lib/first-minute.js +2 -2
- package/lib/fleet.js +8 -36
- package/lib/git-spawn.js +5 -2
- package/lib/ignored-cache.js +51 -0
- package/lib/known-commands.js +1 -1
- package/lib/manifest.js +2 -2
- package/lib/map-refs.js +70 -4
- package/lib/member-engine.js +15 -1
- package/lib/member-scaffold.js +2 -0
- package/lib/member-type.js +27 -0
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-runs.js +13 -2
- package/lib/runner-command.js +54 -3
- package/lib/sandbox-pids.js +113 -0
- package/lib/secret-gateway.js +3 -13
- package/lib/state-home.js +106 -0
- package/lib/task-db.js +4 -3
- package/lib/task-prune.js +462 -0
- package/lib/x-post-check.js +497 -0
- package/package.json +1 -1
- package/scripts/det/data/xsearch-answer.json +24 -0
- package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
- package/scripts/det/xsearch-bench.js +307 -0
- package/scripts/det/ytnotes +122 -16
- package/scripts/det/ytrail-bench.js +215 -0
- package/scripts/det/ytrail-eval.js +128 -56
- package/utils/auth.js +3 -3
- package/utils/update-check.js +2 -2
package/atris/features/README.md
CHANGED
|
@@ -86,6 +86,14 @@ None.
|
|
|
86
86
|
|
|
87
87
|
### Completed Features
|
|
88
88
|
|
|
89
|
+
#### youtube-notes
|
|
90
|
+
Free, fast, checked notes from any YouTube link
|
|
91
|
+
- **Files:** commands/youtube.js, scripts/det/ytnotes, scripts/det/ytquote-repair.js, test/ytnotes.test.js, test/ytquote-repair.test.js, test/youtube.test.js, atris/features/youtube-notes/*
|
|
92
|
+
- **Status:** complete; the `atris youtube bench` scorecard is in review
|
|
93
|
+
- **Keywords:** youtube, notes, captions, transcript, whisper, quotes
|
|
94
|
+
- **What:** `atris youtube notes <url>` reads the real English captions, transcribes on an Apple silicon Mac when there are none, writes with the fastest installed AI, and checks every quote against the transcript
|
|
95
|
+
- **Completed:** 2026-09-28
|
|
96
|
+
|
|
89
97
|
#### audit-gaps
|
|
90
98
|
Close remaining audit gaps from self-audit
|
|
91
99
|
- **Files:** atris/team/*/MEMBER.md, atris/features/README.md, atris/features/audit-gaps/*
|
package/atris/skills/README.md
CHANGED
|
@@ -34,6 +34,7 @@ cp -r atris/skills/[name] ~/.codex/skills/
|
|
|
34
34
|
| backend | Backend architecture policy | `policies/atris-backend.md` |
|
|
35
35
|
| blocks | Author Atris block documents: docs, decks, reports | - |
|
|
36
36
|
| calendar | Google Calendar via AtrisOS API | - |
|
|
37
|
+
| commerce | Quote a purchase or stock order; the person approves before anything is charged | - |
|
|
37
38
|
| copy-editor | Detects and fixes AI writing patterns | - |
|
|
38
39
|
| create-app | Build and deploy an Atris app from a description | - |
|
|
39
40
|
| create-member | Create and manage MEMBER.md team members | - |
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commerce
|
|
3
|
+
description: "Buy or trade through Atris for a person: get a quote, show it, the person approves, check status, read their transactions. Use when asked to buy tickets, buy an item from a store, place a stock order, or list what someone bought or traded. Triggers on buy, purchase, order, trade, stock order, transactions, what did I spend."
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
tags:
|
|
6
|
+
- commerce
|
|
7
|
+
- money
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Commerce: buy or trade through Atris
|
|
11
|
+
|
|
12
|
+
You can quote and ask. Only the human can pay. Every dollar leaves only after
|
|
13
|
+
the person acts themselves: a tap in their Link app, a payment page they
|
|
14
|
+
fill in, or a text from their own phone.
|
|
15
|
+
|
|
16
|
+
## Get a key
|
|
17
|
+
|
|
18
|
+
The person mints a commerce key from their own signed-in CLI. It can ask for
|
|
19
|
+
prices and start their approval. It can never pay.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
atris login --agent --commerce --commerce-max-usd 150
|
|
23
|
+
# scopes: commerce:quote, transactions:read
|
|
24
|
+
# largest single quote: $150 (1 to 500, default 200)
|
|
25
|
+
# this key can ask for prices and start your approval, it can never pay.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- Same machine: nothing else to do. The key is stored in its own slot
|
|
29
|
+
(`commerce_agent_token` in `~/.atris/credentials.json`), and `atris buy` and
|
|
30
|
+
`atris transactions` use it. Minting an x-search or YouTube key later does not
|
|
31
|
+
replace it.
|
|
32
|
+
- Agent on another machine: add `--print-key` to show the key once. The person
|
|
33
|
+
hands it to the agent as `ATRIS_TOKEN` (CLI) or `ATRIS_API_KEY` (curl below).
|
|
34
|
+
Never paste it into a chat.
|
|
35
|
+
- Narrower key: `--scopes commerce:quote` (quote, approve, status) or
|
|
36
|
+
`--scopes transactions:read` (history only).
|
|
37
|
+
- Quotes over the key's cap are refused, and each key gets 50 quotes a day (UTC). A developer key (`atris api-key`) or the
|
|
38
|
+
person's own login also works, without those per-key limits.
|
|
39
|
+
|
|
40
|
+
## The loop
|
|
41
|
+
|
|
42
|
+
1. Quote. `atris buy quote --kind <kind> [fields]`.
|
|
43
|
+
2. Show the person the summary, total, fee and approval text. Word for word on money.
|
|
44
|
+
3. Only after they say yes, approve. `atris buy approve <id> [fields]`.
|
|
45
|
+
4. Hand the person the approval link (Link or payment page). For trades, tell them to check their texts: Atris sends the one-time code only to their own text thread, never to you. Nothing else pays.
|
|
46
|
+
5. Check `atris buy status <id>` until the state is `done`, `failed`, `refunded` or `unknown`.
|
|
47
|
+
6. `atris transactions` lists everything they bought, paid in fees, traded or sent.
|
|
48
|
+
|
|
49
|
+
Add `--json` to any command for the raw response.
|
|
50
|
+
|
|
51
|
+
## Kinds
|
|
52
|
+
|
|
53
|
+
| kind | quote flags | approve flags | human approves by |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| `ticket` | `--event-url` (vividseats.com), `--quantity`, `--min-each-usd`, `--max-each-usd` | `--pick` (1-3 or ticket_id), `--delivery-email` | Link app, or payment page |
|
|
56
|
+
| `shop` | `--query` or `--url`, `--quantity` | `--options '<json>'`, `--delivery-email`, `--address '<json>'` (US, once), `--accept-extras` | Link app |
|
|
57
|
+
| `trade` | `--side` buy/sell, `--symbol`, `--quantity` or `--dollars`, `--order-type` market/limit, `--limit-price` | none | replying to Atris's text with the exact phrase from their own phone, within 5 minutes |
|
|
58
|
+
| `flight` | not wired yet, quotes are refused | | |
|
|
59
|
+
|
|
60
|
+
Any field can also go in as JSON: `--intent '{"kind":"shop","query":"navy tee"}'` for quotes,
|
|
61
|
+
`--details '{"pick":"1"}'` for approve. Flags win over the JSON.
|
|
62
|
+
|
|
63
|
+
## Rails (enforced in code, not by you)
|
|
64
|
+
|
|
65
|
+
- Approval is always human. An API key cannot confirm a trade or approve a Link spend.
|
|
66
|
+
- Caps: purchases up to `TEXT_TICKETS_MAX_USD` (default $400) and $500 per Link spend. Stocks: per-order and per-day caps, the Robinhood risk policy, market hours, no shorting, stocks only.
|
|
67
|
+
- Quotes expire: 30 minutes for purchases, 10 for trades. A trade quote stages at most one order. Ask again after that.
|
|
68
|
+
- A trade already waiting for the person's yes blocks a new one. Let them confirm or let it expire.
|
|
69
|
+
- No retries. If a result is `unknown`, money may have moved. Tell the person to check the store or Robinhood app. Never buy again to "fix" it.
|
|
70
|
+
- No advice on trades. Carry out only the person's own instruction.
|
|
71
|
+
- For Link-mode orders, status may carry `fee_payment_url`: the page where the person pays the Atris fee. The CLI prints it as "Pay the Atris fee".
|
|
72
|
+
- Status values from quote and approve: `quoted`, `pending_approval`, `needs_input` (ask the person for the named field), `needs_setup` (send them the connect link), `refused` (read the message to them).
|
|
73
|
+
|
|
74
|
+
## With the CLI
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Tickets
|
|
78
|
+
atris buy quote --kind ticket --event-url "https://www.vividseats.com/..." --quantity 2
|
|
79
|
+
atris buy approve ord_<uuid> --pick 1 --delivery-email sam@example.com
|
|
80
|
+
# -> Approve in Link: https://... Nothing has been charged yet.
|
|
81
|
+
|
|
82
|
+
# A store item
|
|
83
|
+
atris buy quote --kind shop --query "uniqlo supima crew tee navy" --quantity 1
|
|
84
|
+
|
|
85
|
+
# A stock order
|
|
86
|
+
atris buy quote --kind trade --side buy --symbol VTI --dollars 25
|
|
87
|
+
atris buy approve trq_<token>
|
|
88
|
+
# -> We texted you a confirm code. Reply to that text to place the trade.
|
|
89
|
+
|
|
90
|
+
# Status and history
|
|
91
|
+
atris buy status ord_<uuid>
|
|
92
|
+
atris transactions --since 2026-09-01 --kind ticket,shop,trade --limit 50
|
|
93
|
+
# Quotes nobody approved are left out; add --include-quotes to see them.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## With curl
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
KEY="Authorization: Bearer $ATRIS_API_KEY"
|
|
100
|
+
API=https://api.atris.ai/api
|
|
101
|
+
|
|
102
|
+
curl -s -X POST $API/commerce/quote -H "$KEY" -H 'content-type: application/json' \
|
|
103
|
+
-d '{"kind":"ticket","event_url":"https://www.vividseats.com/...","quantity":2}'
|
|
104
|
+
curl -s -X POST $API/commerce/ord_<uuid>/approve -H "$KEY" -H 'content-type: application/json' \
|
|
105
|
+
-d '{"pick":"1","delivery_email":"sam@example.com"}'
|
|
106
|
+
# -> {"status":"pending_approval","approval":{"how":"link_app","url":"https://..."}}
|
|
107
|
+
|
|
108
|
+
curl -s -X POST $API/commerce/quote -H "$KEY" -H 'content-type: application/json' \
|
|
109
|
+
-d '{"kind":"trade","side":"buy","symbol":"VTI","dollars":25}'
|
|
110
|
+
curl -s -X POST $API/commerce/trq_<token>/approve -H "$KEY"
|
|
111
|
+
# -> {"status":"pending_approval","approval":{"how":"text_confirm","text":"Check your texts. ..."}}
|
|
112
|
+
|
|
113
|
+
curl -s $API/commerce/ord_<uuid> -H "$KEY"
|
|
114
|
+
curl -s "$API/transactions?since=2026-09-01&kinds=ticket,shop,trade&limit=50" -H "$KEY"
|
|
115
|
+
# Quotes nobody approved are left out; add &include_quotes=true to see them.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each transaction: `id, kind, when, what, amount_cents, currency, state, counterparty, receipt_url`.
|
|
119
|
+
`sources` says which ledgers loaded; if one says `failed`, the list is partial. Say so.
|
|
120
|
+
The CLI prints that as "Partial list: could not load ...".
|
|
@@ -49,7 +49,9 @@ atris x-search person --name "Leah Bonvissuto" --handle leahbon
|
|
|
49
49
|
atris x-search --help
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Every cited post is looked up on X for free (the public embed endpoint, 5s cap), whatever layout the answer uses. A cited post whose words appear in the answer is checked: its `https://x.com/<handle>/status/<id> (checked)` link prints under that post, or in a `Checked posts:` list when the answer is a table or prose. A quote of 40+ characters that matches no real post says `(could not find this post on X, treat as unverified)`; if X could not be reached it says so and is never called fake. Citations no post uses print as `Other sources:`. The last line is `posts: N checked, N unverified, N unknown`. `--no-check` skips the lookup. Bench it with `atris x-search bench --free` (0 credits) or `--quick` (5 credits).
|
|
53
|
+
|
|
54
|
+
Add `--json` for the raw API payload (plus a `checks` list). Curl below is the raw API for debugging only.
|
|
53
55
|
|
|
54
56
|
## Bootstrap (ALWAYS Run First)
|
|
55
57
|
|
package/ax
CHANGED
|
@@ -5,6 +5,7 @@ const http = require('http');
|
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { stateHome } = require('./lib/state-home');
|
|
8
9
|
const readline = require('readline');
|
|
9
10
|
const crypto = require('crypto');
|
|
10
11
|
const { Readable } = require('stream');
|
|
@@ -230,7 +231,7 @@ function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', out
|
|
|
230
231
|
|
|
231
232
|
const baseDir = fs.existsSync(path.join(cwd, 'atris'))
|
|
232
233
|
? path.join(cwd, 'atris', 'runs')
|
|
233
|
-
: path.join(
|
|
234
|
+
: path.join(stateHome(), '.atris', 'runs');
|
|
234
235
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
235
236
|
const logPath = path.join(baseDir, `ax-${kind}-${timestampForFile()}.log`);
|
|
236
237
|
fs.appendFileSync(logPath, [
|
|
@@ -995,7 +996,7 @@ function normalizeMode(mode) {
|
|
|
995
996
|
function autoPickLogPath(options = {}) {
|
|
996
997
|
return options.pickLogPath
|
|
997
998
|
|| process.env.ATRIS_ROUTER_PICKS_PATH
|
|
998
|
-
|| path.join(
|
|
999
|
+
|| path.join(stateHome(), '.atris', 'ax-auto-picks.jsonl');
|
|
999
1000
|
}
|
|
1000
1001
|
|
|
1001
1002
|
function autoMessageWords(message) {
|
|
@@ -2328,7 +2329,7 @@ function formatTaskPreviewRows(receipt, options = {}) {
|
|
|
2328
2329
|
}
|
|
2329
2330
|
|
|
2330
2331
|
function approvalStorePath(options = {}) {
|
|
2331
|
-
return options.storePath || process.env.AX_APPROVAL_STORE || path.join(
|
|
2332
|
+
return options.storePath || process.env.AX_APPROVAL_STORE || path.join(stateHome(), '.atris', 'ax-approvals.json');
|
|
2332
2333
|
}
|
|
2333
2334
|
|
|
2334
2335
|
function approvalMaxAgeMs(options = {}) {
|
package/bin/atris.js
CHANGED
|
@@ -717,6 +717,8 @@ function showHelpAll() {
|
|
|
717
717
|
console.log(' usage - Show developer API usage');
|
|
718
718
|
console.log(' api-key - Create, list, rotate, or revoke a developer API key');
|
|
719
719
|
console.log(' topup - Buy credits and print a Stripe checkout URL');
|
|
720
|
+
console.log(' buy - Quote a purchase or stock order; the person approves before anything is charged');
|
|
721
|
+
console.log(' transactions - List what you bought, paid in fees, traded, or sent');
|
|
720
722
|
console.log(' design - Extract a site design system, check brand adherence, search brands');
|
|
721
723
|
console.log(' mcp - Run the atris MCP server (stdio) for Claude Desktop and Cursor');
|
|
722
724
|
console.log('');
|
|
@@ -1009,6 +1011,9 @@ function showAuthHelp(commandName) {
|
|
|
1009
1011
|
console.log(' --agent Mint a scoped agent token from the stored login.');
|
|
1010
1012
|
console.log(' --scopes <list> Comma-separated scopes (default: x-search,youtube).');
|
|
1011
1013
|
console.log(' --daily-credit-cap <n> Daily credit cap (default: 50).');
|
|
1014
|
+
console.log(' --commerce Commerce key: scopes commerce:quote,transactions:read. Can quote, never pay.');
|
|
1015
|
+
console.log(' --commerce-max-usd <n> Largest single quote a commerce key may ask for, 1 to 500 (default: 200).');
|
|
1016
|
+
console.log(' --print-key Print the minted key once, for an agent on another machine.');
|
|
1012
1017
|
} else if (commandName === 'switch') {
|
|
1013
1018
|
console.log(' --global, -g Switch the account for all terminals.');
|
|
1014
1019
|
}
|
|
@@ -1854,9 +1859,15 @@ function showWelcomeVisualization() {
|
|
|
1854
1859
|
// Every new chat sees which tool and model does each job, from the same
|
|
1855
1860
|
// resolver as atris engine roster. A roster that cannot be read drops the
|
|
1856
1861
|
// line; it never stops the boot.
|
|
1862
|
+
// A roster worker that ends within seven days gets one line, so the job
|
|
1863
|
+
// does not quietly fall to its backup when the date passes.
|
|
1857
1864
|
try {
|
|
1858
|
-
const
|
|
1865
|
+
const engineView = require('../commands/engine');
|
|
1866
|
+
const jobs = engineView.jobRosterView(cwd);
|
|
1867
|
+
const teamLine = require('../lib/team-lineup').bootTeamLine(cwd, { width: 80 - 11, jobs });
|
|
1859
1868
|
if (teamLine) console.log(row('team', teamLine));
|
|
1869
|
+
const endsLine = engineView.bootExpiringLine(engineView.expiringWorkers(jobs));
|
|
1870
|
+
if (endsLine) console.log(row('roster', endsLine));
|
|
1860
1871
|
} catch {
|
|
1861
1872
|
// Silent: the lineup is a hint, not a gate.
|
|
1862
1873
|
}
|
|
@@ -2368,6 +2379,15 @@ if (command === 'guide') {
|
|
|
2368
2379
|
Promise.resolve(require('../commands/topup').topupCommand(process.argv.slice(3)))
|
|
2369
2380
|
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2370
2381
|
.catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
|
|
2382
|
+
} else if (command === 'buy') {
|
|
2383
|
+
// Buy: quote, then hand the person the approval step. Never charges.
|
|
2384
|
+
Promise.resolve(require('../commands/buy').buyCommand(process.argv.slice(3)))
|
|
2385
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2386
|
+
.catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
|
|
2387
|
+
} else if (command === 'transactions') {
|
|
2388
|
+
Promise.resolve(require('../commands/transactions').transactionsCommand(process.argv.slice(3)))
|
|
2389
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2390
|
+
.catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
|
|
2371
2391
|
} else if (command === '_resolve') {
|
|
2372
2392
|
// Hidden: resolve a profile name query → print exact profile name
|
|
2373
2393
|
require('../commands/auth').resolveProfile();
|
|
@@ -2970,7 +2990,7 @@ if (command === 'guide') {
|
|
|
2970
2990
|
const subcommand = process.argv[3];
|
|
2971
2991
|
const args = process.argv.slice(4);
|
|
2972
2992
|
Promise.resolve(require('../commands/member').memberCommand(subcommand, ...args))
|
|
2973
|
-
.then(() => process.exit(
|
|
2993
|
+
.then(() => process.exit(process.exitCode || 0))
|
|
2974
2994
|
.catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2975
2995
|
} else if (command === 'app') {
|
|
2976
2996
|
const subcommand = process.argv[3];
|
package/cli/atris_code.py
CHANGED
|
@@ -12,11 +12,13 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
14
|
import asyncio
|
|
15
|
+
import getpass
|
|
15
16
|
import json
|
|
16
17
|
import logging
|
|
17
18
|
import os
|
|
18
19
|
import subprocess
|
|
19
20
|
import sys
|
|
21
|
+
import tempfile
|
|
20
22
|
import time
|
|
21
23
|
from dataclasses import dataclass
|
|
22
24
|
from pathlib import Path
|
|
@@ -36,7 +38,29 @@ RED = "\033[91m"
|
|
|
36
38
|
|
|
37
39
|
PACKAGE_ROOT = Path(__file__).resolve().parent.parent
|
|
38
40
|
TEAM_DIR = PACKAGE_ROOT / "atris" / "team"
|
|
39
|
-
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _same_dir(a: str, b: str) -> bool:
|
|
44
|
+
try:
|
|
45
|
+
return os.path.samefile(a, b)
|
|
46
|
+
except OSError:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _state_home() -> Path:
|
|
51
|
+
"""Mirror lib/state-home.js: a test run never writes the real ~/.atris."""
|
|
52
|
+
env, home = os.environ, Path.home()
|
|
53
|
+
under_test = env.get("NODE_TEST_CONTEXT") or env.get("ATRIS_TEST_STATE_DIR")
|
|
54
|
+
if env.get("ATRIS_TEST_REAL_HOME") == "1" or not under_test:
|
|
55
|
+
return home
|
|
56
|
+
real = [os.path.expanduser("~" + getpass.getuser()), env.get("ATRIS_TEST_PROTECTED_HOME", "")]
|
|
57
|
+
if not any(r and _same_dir(str(home), r) for r in real):
|
|
58
|
+
return home
|
|
59
|
+
user = os.getuid() if hasattr(os, "getuid") else getpass.getuser()
|
|
60
|
+
return Path(env.get("ATRIS_TEST_STATE_DIR") or os.path.join(tempfile.gettempdir(), f"atris-test-home-{user}"))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
STATE_DIR = _state_home() / ".atris"
|
|
40
64
|
SESSION_STATE_FILE = STATE_DIR / "computer_sessions.json"
|
|
41
65
|
AUDIT_LOG_FILE = STATE_DIR / "computer_audit.jsonl"
|
|
42
66
|
RESUME_MAX_AGE_SECONDS = 6 * 60 * 60
|
package/commands/auth.js
CHANGED
|
@@ -8,6 +8,13 @@ const path = require('path');
|
|
|
8
8
|
const AGENT_TOKEN_PATH = '/auth/agent-token';
|
|
9
9
|
const DEFAULT_AGENT_SCOPES = ['x-search', 'youtube'];
|
|
10
10
|
const DEFAULT_DAILY_CREDIT_CAP = 50;
|
|
11
|
+
// Commerce keys can ask for prices and start the owner's approval. They can
|
|
12
|
+
// never pay: every dollar still needs the owner's own tap or text. They live
|
|
13
|
+
// in their own slot so an x-search or YouTube mint never overwrites them.
|
|
14
|
+
const COMMERCE_QUOTE_SCOPE = 'commerce:quote';
|
|
15
|
+
const COMMERCE_SCOPES = [COMMERCE_QUOTE_SCOPE, 'transactions:read'];
|
|
16
|
+
const COMMERCE_MAX_USD_LIMIT = 500;
|
|
17
|
+
const COMMERCE_NEVER_PAYS = 'this key can ask for prices and start your approval, it can never pay.';
|
|
11
18
|
const NO_STORED_JWT_MESSAGE = 'not signed in. run atris login first.';
|
|
12
19
|
|
|
13
20
|
function firstNonEmptyString(...values) {
|
|
@@ -23,8 +30,12 @@ function wantsAgentToken(args = []) {
|
|
|
23
30
|
return first === 'agent-token';
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
function
|
|
27
|
-
|
|
33
|
+
function isCommerceKey(scopes = []) {
|
|
34
|
+
return Array.isArray(scopes) && scopes.some((scope) => COMMERCE_SCOPES.includes(String(scope)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseScopeList(raw, fallback = DEFAULT_AGENT_SCOPES) {
|
|
38
|
+
if (!raw) return [...fallback];
|
|
28
39
|
const scopes = String(raw).split(',').map((part) => part.trim()).filter(Boolean);
|
|
29
40
|
if (scopes.length === 0) {
|
|
30
41
|
throw new Error('scopes must list at least one value');
|
|
@@ -43,11 +54,29 @@ function parseDailyCreditCap(args) {
|
|
|
43
54
|
return value;
|
|
44
55
|
}
|
|
45
56
|
|
|
57
|
+
function parseCommerceMaxUsd(args, scopes) {
|
|
58
|
+
const present = args.some((arg) => arg === '--commerce-max-usd' || String(arg).startsWith('--commerce-max-usd='));
|
|
59
|
+
if (!present) return null;
|
|
60
|
+
const raw = String(readFlag(args, '--commerce-max-usd', '')).replace(/^\$/, '');
|
|
61
|
+
const value = Number(raw);
|
|
62
|
+
if (!Number.isInteger(value) || value < 1 || value > COMMERCE_MAX_USD_LIMIT) {
|
|
63
|
+
throw new Error(`commerce max must be a whole dollar amount from 1 to ${COMMERCE_MAX_USD_LIMIT}`);
|
|
64
|
+
}
|
|
65
|
+
if (!scopes.includes(COMMERCE_QUOTE_SCOPE)) {
|
|
66
|
+
throw new Error('--commerce-max-usd needs the commerce:quote scope (try --commerce)');
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
function parseAgentTokenArgs(args = []) {
|
|
72
|
+
const commerce = hasFlag(args, '--commerce');
|
|
73
|
+
const scopes = parseScopeList(readFlag(args, '--scopes', ''), commerce ? COMMERCE_SCOPES : DEFAULT_AGENT_SCOPES);
|
|
47
74
|
return {
|
|
48
75
|
agent: wantsAgentToken(args),
|
|
49
|
-
scopes
|
|
76
|
+
scopes,
|
|
50
77
|
dailyCreditCap: parseDailyCreditCap(args),
|
|
78
|
+
commerceMaxUsd: parseCommerceMaxUsd(args, scopes),
|
|
79
|
+
printKey: hasFlag(args, '--print-key'),
|
|
51
80
|
json: wantsJson(args),
|
|
52
81
|
help: hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help',
|
|
53
82
|
};
|
|
@@ -82,7 +111,14 @@ function extractAgentTokenMeta(data, requested, token) {
|
|
|
82
111
|
const dailyCreditCap = Number.isFinite(Number(rawCap)) ? Number(rawCap) : requested.dailyCreditCap;
|
|
83
112
|
const expiresAt = expiryFromValue(payload.expires_at ?? payload.expiry ?? payload.expires ?? payload.exp)
|
|
84
113
|
|| expiryFromValue(getTokenExpiryEpochSeconds(token));
|
|
85
|
-
|
|
114
|
+
const rawMax = payload.commerce_max_quote_usd;
|
|
115
|
+
const commerceMaxUsd = Number.isFinite(Number(rawMax)) && rawMax !== null && rawMax !== undefined
|
|
116
|
+
? Number(rawMax)
|
|
117
|
+
: (requested.commerceMaxUsd || null);
|
|
118
|
+
const commerceDailyQuotes = Number.isFinite(Number(payload.commerce_daily_quotes)) && payload.commerce_daily_quotes != null
|
|
119
|
+
? Number(payload.commerce_daily_quotes)
|
|
120
|
+
: null;
|
|
121
|
+
return { scopes, dailyCreditCap, expiresAt, commerceMaxUsd, commerceDailyQuotes };
|
|
86
122
|
}
|
|
87
123
|
|
|
88
124
|
function isAgentAccessToken(token) {
|
|
@@ -116,12 +152,22 @@ function persistMintedAgentToken(credentials, token, extras = {}) {
|
|
|
116
152
|
if (isAgentAccessToken(credentials.token)) {
|
|
117
153
|
throw new Error('Refusing to save a scoped agent token as the login token; keep it under agent_token');
|
|
118
154
|
}
|
|
155
|
+
const slot = isCommerceKey(extras.scopes)
|
|
156
|
+
? {
|
|
157
|
+
commerce_agent_token: token,
|
|
158
|
+
commerce_agent_token_scopes: extras.scopes || [],
|
|
159
|
+
commerce_agent_token_expires_at: extras.expiresAt || null,
|
|
160
|
+
commerce_max_quote_usd: extras.commerceMaxUsd || null,
|
|
161
|
+
}
|
|
162
|
+
: {
|
|
163
|
+
agent_token: token,
|
|
164
|
+
agent_token_scopes: extras.scopes || [],
|
|
165
|
+
agent_token_expires_at: extras.expiresAt || null,
|
|
166
|
+
};
|
|
119
167
|
const next = {
|
|
120
168
|
...credentials,
|
|
121
169
|
refresh_token: extras.refresh_token || credentials.refresh_token || null,
|
|
122
|
-
|
|
123
|
-
agent_token_scopes: extras.scopes || [],
|
|
124
|
-
agent_token_expires_at: extras.expiresAt || null,
|
|
170
|
+
...slot,
|
|
125
171
|
};
|
|
126
172
|
if (credentials.source_profile) {
|
|
127
173
|
saveProfile(credentials.source_profile, next);
|
|
@@ -132,10 +178,14 @@ function persistMintedAgentToken(credentials, token, extras = {}) {
|
|
|
132
178
|
}
|
|
133
179
|
|
|
134
180
|
function printAgentTokenMint(meta, output) {
|
|
135
|
-
|
|
181
|
+
const commerce = isCommerceKey(meta.scopes);
|
|
182
|
+
output(commerce ? 'minted commerce agent key' : 'minted scoped agent token');
|
|
136
183
|
output(`scopes: ${meta.scopes.join(', ')}`);
|
|
184
|
+
if (meta.commerceMaxUsd) output(`largest single quote: $${meta.commerceMaxUsd}`);
|
|
185
|
+
if (meta.commerceDailyQuotes) output(`quotes per day: ${meta.commerceDailyQuotes}`);
|
|
137
186
|
output(`daily credit cap: ${meta.dailyCreditCap}`);
|
|
138
187
|
if (meta.expiresAt) output(`expires: ${meta.expiresAt}`);
|
|
188
|
+
if (commerce) output(COMMERCE_NEVER_PAYS);
|
|
139
189
|
}
|
|
140
190
|
|
|
141
191
|
function redactSecret(text, secret) {
|
|
@@ -175,10 +225,16 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
|
|
|
175
225
|
return { ok: false, code: 'not_logged_in', error: NO_STORED_JWT_MESSAGE };
|
|
176
226
|
}
|
|
177
227
|
|
|
228
|
+
const commerceMaxUsd = Number.isInteger(requested.commerceMaxUsd) ? requested.commerceMaxUsd : null;
|
|
229
|
+
if (commerceMaxUsd !== null && !scopes.includes(COMMERCE_QUOTE_SCOPE)) {
|
|
230
|
+
return { ok: false, code: 'invalid_scopes', error: '--commerce-max-usd needs the commerce:quote scope' };
|
|
231
|
+
}
|
|
232
|
+
|
|
178
233
|
const body = {
|
|
179
234
|
scopes,
|
|
180
235
|
daily_credit_cap: dailyCreditCap,
|
|
181
236
|
};
|
|
237
|
+
if (commerceMaxUsd !== null) body.commerce_max_quote_usd = commerceMaxUsd;
|
|
182
238
|
let authToken = isAgentAccessToken(accessToken) ? refreshToken : (accessToken || refreshToken);
|
|
183
239
|
let result = await postAgentToken(api, authToken, body);
|
|
184
240
|
if (!result.ok && result.status === 401 && refreshToken && authToken !== refreshToken) {
|
|
@@ -201,7 +257,7 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
|
|
|
201
257
|
return { ok: false, code: 'missing_token', error: 'backend did not return an agent token' };
|
|
202
258
|
}
|
|
203
259
|
|
|
204
|
-
const meta = extractAgentTokenMeta(result.data, { scopes, dailyCreditCap }, minted);
|
|
260
|
+
const meta = extractAgentTokenMeta(result.data, { scopes, dailyCreditCap, commerceMaxUsd }, minted);
|
|
205
261
|
persist(credentials, minted, {
|
|
206
262
|
...meta,
|
|
207
263
|
refresh_token: firstNonEmptyString(result.data && result.data.refresh_token) || refreshToken,
|
|
@@ -212,7 +268,7 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
|
|
|
212
268
|
ok: true,
|
|
213
269
|
token: minted,
|
|
214
270
|
meta,
|
|
215
|
-
storedIn: credentials.source_profile ? `profile ${credentials.source_profile}
|
|
271
|
+
storedIn: `${credentials.source_profile ? `profile ${credentials.source_profile}` : '~/.atris/credentials.json'}, ${isCommerceKey(meta.scopes) ? 'commerce_agent_token' : 'agent_token'}`,
|
|
216
272
|
};
|
|
217
273
|
}
|
|
218
274
|
|
|
@@ -276,6 +332,7 @@ async function mintAgentToken(args = [], deps = {}) {
|
|
|
276
332
|
const minted = await mintScopedAgentToken({
|
|
277
333
|
scopes: options.scopes,
|
|
278
334
|
dailyCreditCap: options.dailyCreditCap,
|
|
335
|
+
commerceMaxUsd: options.commerceMaxUsd,
|
|
279
336
|
}, deps);
|
|
280
337
|
|
|
281
338
|
if (!minted.ok) {
|
|
@@ -302,12 +359,24 @@ async function mintAgentToken(args = [], deps = {}) {
|
|
|
302
359
|
scopes: minted.meta.scopes,
|
|
303
360
|
daily_credit_cap: minted.meta.dailyCreditCap,
|
|
304
361
|
expires_at: minted.meta.expiresAt,
|
|
362
|
+
...(isCommerceKey(minted.meta.scopes) ? {
|
|
363
|
+
commerce_max_quote_usd: minted.meta.commerceMaxUsd,
|
|
364
|
+
commerce_daily_quotes: minted.meta.commerceDailyQuotes,
|
|
365
|
+
can_pay: false,
|
|
366
|
+
note: COMMERCE_NEVER_PAYS,
|
|
367
|
+
} : {}),
|
|
368
|
+
...(options.printKey ? { key: minted.token } : {}),
|
|
305
369
|
}, null, 2));
|
|
306
370
|
return 0;
|
|
307
371
|
}
|
|
308
372
|
|
|
309
373
|
printAgentTokenMint(minted.meta, output);
|
|
310
374
|
output(`stored in ${minted.storedIn}`);
|
|
375
|
+
if (options.printKey) {
|
|
376
|
+
// Only on request: an outside agent needs the raw key once.
|
|
377
|
+
output('key (shown once, give it to the agent as ATRIS_TOKEN, do not paste it into chat):');
|
|
378
|
+
output(minted.token);
|
|
379
|
+
}
|
|
311
380
|
return 0;
|
|
312
381
|
}
|
|
313
382
|
|
package/commands/autoland.js
CHANGED
|
@@ -135,7 +135,16 @@ function runOwnCli(root, cliArgs, timeoutMs = 300000) {
|
|
|
135
135
|
// target root or a global `atris` breaks whenever the tick runs a project
|
|
136
136
|
// that isn't the CLI repo on a machine without a global install (CI, cron).
|
|
137
137
|
const bin = path.resolve(__dirname, '..', 'bin', 'atris.js');
|
|
138
|
-
|
|
138
|
+
return spawnCapture([bin, ...cliArgs], { cwd: root, timeoutMs });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// spawnSync keeps 1 MB of stdout by default and kills the child past it
|
|
142
|
+
// (ENOBUFS). A certify-and-land sweep over 150+ review rows prints more than
|
|
143
|
+
// that, so the accept step read as unreadable on most ticks. Allow 64 MB.
|
|
144
|
+
const OWN_CLI_MAX_BUFFER = 64 * 1024 * 1024;
|
|
145
|
+
|
|
146
|
+
function spawnCapture(nodeArgs, { cwd, timeoutMs = 300000 } = {}) {
|
|
147
|
+
const result = spawnSync(process.execPath, nodeArgs, { cwd, encoding: 'utf8', timeout: timeoutMs, maxBuffer: OWN_CLI_MAX_BUFFER });
|
|
139
148
|
return {
|
|
140
149
|
status: result.status,
|
|
141
150
|
stdout: String(result.stdout || ''),
|
|
@@ -1139,6 +1148,7 @@ module.exports = {
|
|
|
1139
1148
|
digestNextMoves,
|
|
1140
1149
|
acceptSweepTimeoutMs,
|
|
1141
1150
|
describeAcceptKillCause,
|
|
1151
|
+
spawnCapture,
|
|
1142
1152
|
digestStoryRows,
|
|
1143
1153
|
digestTickStatus,
|
|
1144
1154
|
engineAnswerValidationLine,
|
package/commands/browse.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const { stateHome } = require('../lib/state-home');
|
|
4
4
|
const { loadCredentials } = require('../utils/auth');
|
|
5
5
|
const { apiRequestJson } = require('../utils/api');
|
|
6
6
|
|
|
@@ -34,7 +34,7 @@ async function browseAtris() {
|
|
|
34
34
|
if (!result.ok) {
|
|
35
35
|
// API failed, try local templates
|
|
36
36
|
console.log(' API unavailable, checking local templates...');
|
|
37
|
-
const localDir = path.join(
|
|
37
|
+
const localDir = path.join(stateHome(), '.atris', 'templates');
|
|
38
38
|
if (!fs.existsSync(localDir)) {
|
|
39
39
|
console.error(' No templates found locally or from API.');
|
|
40
40
|
process.exit(1);
|
package/commands/business.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { stateHome } = require('../lib/state-home');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
const { loadCredentials } = require('../utils/auth');
|
|
5
6
|
const { apiRequestJson } = require('../utils/api');
|
|
@@ -18,8 +19,7 @@ function businessWorkspaceBase() {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function getBusinessConfigPath() {
|
|
21
|
-
const
|
|
22
|
-
const dir = path.join(home, '.atris');
|
|
22
|
+
const dir = path.join(stateHome(), '.atris');
|
|
23
23
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
24
24
|
return path.join(dir, 'businesses.json');
|
|
25
25
|
}
|
|
@@ -3139,7 +3139,7 @@ async function connectService(connector, ...flags) {
|
|
|
3139
3139
|
}
|
|
3140
3140
|
|
|
3141
3141
|
// Create local secrets directory
|
|
3142
|
-
const secretsDir = path.join(
|
|
3142
|
+
const secretsDir = path.join(stateHome(), '.atris', 'secrets', connector);
|
|
3143
3143
|
if (!fs.existsSync(secretsDir)) {
|
|
3144
3144
|
fs.mkdirSync(secretsDir, { recursive: true });
|
|
3145
3145
|
console.log(`\n Created secrets dir: ${secretsDir}/`);
|