argus-decision-mcp 1.0.0 → 1.3.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/LICENSE +1 -1
- package/README.md +135 -41
- package/SECURITY.md +22 -8
- package/dist/a0/account-connect.js +180 -0
- package/dist/a0/account-credentials.js +86 -0
- package/dist/index.js +35 -1
- package/dist/lib/ambient-due.js +63 -0
- package/dist/lib/ambient-elicit.js +225 -0
- package/dist/lib/argus-dir.js +76 -9
- package/dist/lib/calendar.js +75 -0
- package/dist/lib/canonical-scales.js +105 -0
- package/dist/lib/due-note.js +62 -13
- package/dist/lib/elicit.js +13 -1
- package/dist/lib/envelope.js +19 -4
- package/dist/lib/install-id.js +53 -0
- package/dist/lib/layout.js +7 -0
- package/dist/lib/ledger-append.js +134 -3
- package/dist/lib/ledger-replay.js +139 -11
- package/dist/lib/locale-mismatch.js +52 -0
- package/dist/lib/locale.js +120 -4
- package/dist/lib/localize-result.js +120 -0
- package/dist/lib/localized-message.js +13 -0
- package/dist/lib/log.js +2 -2
- package/dist/lib/numeric-drift.js +343 -22
- package/dist/lib/package-meta.js +14 -0
- package/dist/lib/premise-sync.js +23 -0
- package/dist/lib/premises-core.js +174 -0
- package/dist/lib/premises.js +51 -50
- package/dist/lib/privacy.js +1 -1
- package/dist/lib/push-account.js +28 -4
- package/dist/lib/receipt.js +5 -1
- package/dist/lib/render-receipt.js +32 -22
- package/dist/lib/resolve-today.js +11 -3
- package/dist/lib/review/extract-core.js +345 -0
- package/dist/lib/review/extract-file-node.js +288 -0
- package/dist/lib/review/index.js +1 -1
- package/dist/lib/review/ingest.js +84 -20
- package/dist/lib/review/lenses.js +18 -0
- package/dist/lib/review/prompts.js +254 -15
- package/dist/lib/review/render.js +86 -38
- package/dist/lib/review/reviewability.js +8 -7
- package/dist/lib/review/routing.js +38 -17
- package/dist/lib/review/schema.js +4 -1
- package/dist/lib/review-path.js +108 -0
- package/dist/lib/spine.js +16 -12
- package/dist/lib/state-machine.js +59 -13
- package/dist/lib/surface-lint.js +97 -0
- package/dist/lib/surfaces.js +313 -48
- package/dist/lib/telemetry.js +169 -0
- package/dist/lib/tool-presentation.js +35 -0
- package/dist/lib/untrusted.js +89 -0
- package/dist/lib/validate-crux.js +32 -7
- package/dist/resources.js +52 -14
- package/dist/server.js +105 -42
- package/dist/tools/amend-dismiss.js +46 -9
- package/dist/tools/candidates.js +129 -0
- package/dist/tools/check-in.js +178 -20
- package/dist/tools/errors.js +17 -3
- package/dist/tools/index.js +36 -2
- package/dist/tools/init-config.js +83 -11
- package/dist/tools/open-decision.js +71 -36
- package/dist/tools/premises.js +246 -29
- package/dist/tools/public-tools.js +382 -0
- package/dist/tools/recall.js +158 -19
- package/dist/tools/recheck.js +51 -15
- package/dist/tools/review.js +234 -33
- package/dist/tools/seal.js +167 -31
- package/dist/tools/semantic-record.js +225 -0
- package/dist/tools/settle.js +204 -20
- package/dist/tools/sync.js +245 -13
- package/dist/tools/tool-types.js +174 -0
- package/dist/tools/watch.js +117 -0
- package/dist/v2/bridge.js +240 -0
- package/dist/v2/brief.js +122 -0
- package/dist/v2/candidate-capture.js +150 -0
- package/dist/v2/capture-cli.js +72 -0
- package/dist/v2/capture-runtime.js +73 -0
- package/dist/v2/connection-io.js +47 -0
- package/dist/v2/connection.js +93 -0
- package/dist/v2/events.js +187 -0
- package/dist/v2/evidence.js +55 -0
- package/dist/v2/gate-keywords.js +48 -0
- package/dist/v2/gate.js +98 -0
- package/dist/v2/git-discovery.js +80 -0
- package/dist/v2/harvest.js +122 -0
- package/dist/v2/init.js +42 -0
- package/dist/v2/ledger.js +290 -0
- package/dist/v2/lifecycle-cli.js +50 -0
- package/dist/v2/lifecycle.js +418 -0
- package/dist/v2/logbook.js +142 -0
- package/dist/v2/mirror.js +305 -0
- package/dist/v2/outbox.js +52 -0
- package/dist/v2/queue.js +183 -0
- package/dist/v2/reducer.js +462 -0
- package/dist/v2/sanitize.js +44 -0
- package/dist/v2/v1-reader.js +300 -0
- package/dist/v3/fixtures/dkk-corpus.js +55 -0
- package/dist/v3/fixtures/p5-measurement-plan.js +42 -0
- package/dist/v3/index.js +5 -0
- package/dist/v3/legacy-v2.js +169 -0
- package/dist/v3/p5-gate.js +123 -0
- package/dist/v3/reducer.js +290 -0
- package/dist/v3/store.js +133 -0
- package/dist/v3/types.js +193 -0
- package/dist/v4/index.js +5 -0
- package/dist/v4/reducer.js +361 -0
- package/dist/v4/relation-validation.js +40 -0
- package/dist/v4/shadow.js +22 -0
- package/dist/v4/types.js +326 -0
- package/dist/v4/watch.js +18 -0
- package/package.json +20 -6
- package/dist/lib/discipline.js +0 -42
- package/dist/prompts.js +0 -72
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# argus-decision-mcp
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/argus-decision-mcp)
|
|
4
|
+
[](https://www.npmjs.com/package/argus-decision-mcp)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
> **Keeping Judgment Human.**
|
|
8
|
+
|
|
3
9
|
> **Your AI gives you an answer. Argus gives you a receipt — and checks it against reality on the date you set.**
|
|
4
10
|
|
|
5
11
|
Argus is an MCP server for **decision accountability**. Instead of grading your
|
|
@@ -9,11 +15,13 @@ what actually happened. The artifact it produces — a **Judgment Receipt** —
|
|
|
9
15
|
carries one line no other AI tool will: `AI VERDICT … NONE`. The model never
|
|
10
16
|
graded you. Reality did.
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
Runs on any MCP host that supports local **stdio** servers — Claude Desktop,
|
|
19
|
+
Claude Code, and other clients that launch a local process. (Remote-only
|
|
20
|
+
connectors that require an HTTP transport aren't supported yet — see the roadmap.)
|
|
13
21
|
|
|
14
22
|
```
|
|
15
23
|
┌─ ARGUS · JUDGMENT RECEIPT ────────────────────────────────┐
|
|
16
|
-
|
|
24
|
+
Prediction saved 2026-04-02 Result recorded 2026-06-30
|
|
17
25
|
|
|
18
26
|
THE REAL QUESTION
|
|
19
27
|
Can we cut over without a maintenance window users notice?
|
|
@@ -27,19 +35,27 @@ Works with any MCP host — Claude, ChatGPT, Gemini, or any MCP-compatible clien
|
|
|
27
35
|
─────────────────────────────────────────────────────────
|
|
28
36
|
AI VERDICT ON THIS DECISION ······················ NONE
|
|
29
37
|
The model never graded you. Reality did.
|
|
30
|
-
|
|
38
|
+
└────────────────────────── argus · prediction → reality ─┘
|
|
31
39
|
```
|
|
32
40
|
|
|
33
41
|
## Why it's different
|
|
34
42
|
|
|
43
|
+
**It is not a receipt of what your *agent* did.** A growing set of tools now
|
|
44
|
+
log an AI coding run — prompt captured, files touched, checkpoint saved, replay
|
|
45
|
+
path written down. That receipt is about the *machine's* actions over one run.
|
|
46
|
+
Argus receipts something no run-logger can: *your* judgment call, in your own
|
|
47
|
+
words, opened again on a date you set and settled against **reality** — not
|
|
48
|
+
against a model's opinion, and not against a diff. Machine-action receipts and
|
|
49
|
+
judgment receipts are different primitives; you can keep both.
|
|
50
|
+
|
|
35
51
|
Most decision tools compete on a *better answer*, a *score*, a *confidence*.
|
|
36
52
|
Argus does the opposite, and the opposite is enforced **structurally**, not
|
|
37
53
|
promised in prose:
|
|
38
54
|
|
|
39
55
|
- **There is no verdict tool.** The model cannot grade your decision because no
|
|
40
56
|
`argus_verdict` / `argus_score` tool exists to call. `grep dist/` and see.
|
|
41
|
-
- **You can't
|
|
42
|
-
a prior `
|
|
57
|
+
- **You can't record a result without a saved prediction.** `argus_resolve`
|
|
58
|
+
hard-errors without a prior `argus_predict` — "no judgment without a falsifiable prediction" is a
|
|
43
59
|
precondition, not a suggestion.
|
|
44
60
|
- **State is the ledger, not a flag.** A decision's status is the fold of an
|
|
45
61
|
append-only event log, so it can't be faked by calling tools out of order.
|
|
@@ -52,7 +68,10 @@ Claude Code:
|
|
|
52
68
|
claude mcp add argus -- npx -y argus-decision-mcp
|
|
53
69
|
```
|
|
54
70
|
|
|
55
|
-
Or add to your host's MCP config
|
|
71
|
+
Or add to your host's MCP config. **Zero config works**: with no `env` at all,
|
|
72
|
+
your ledger lives in `~/.argus`.
|
|
73
|
+
|
|
74
|
+
**Claude Code** (expands `${CLAUDE_PROJECT_DIR}`, so a per-project ledger works):
|
|
56
75
|
|
|
57
76
|
```jsonc
|
|
58
77
|
{
|
|
@@ -61,19 +80,21 @@ Or add to your host's MCP config:
|
|
|
61
80
|
"command": "npx",
|
|
62
81
|
"args": ["-y", "argus-decision-mcp"],
|
|
63
82
|
"env": {
|
|
64
|
-
//
|
|
65
|
-
// back to it. Claude Code expands ${CLAUDE_PROJECT_DIR}; on other hosts
|
|
66
|
-
// put an absolute path here (or pass argus_dir per call).
|
|
83
|
+
// OPTIONAL — per-project ledger. Omit entirely to use ~/.argus.
|
|
67
84
|
"ARGUS_DIR": "${CLAUDE_PROJECT_DIR}/.argus",
|
|
68
85
|
// OPTIONAL — connect to your Argus account so sealed predictions get an
|
|
69
86
|
// email at their check-by date (the Companion Brief) and show up in the
|
|
70
|
-
// web dashboard. Issue the token in the web app
|
|
71
|
-
// fully local (the privacy-preserving default).
|
|
87
|
+
// web dashboard. Issue the token in the web app (Settings → sync token).
|
|
88
|
+
// Leave it unset to stay fully local (the privacy-preserving default).
|
|
72
89
|
"ARGUS_TOKEN": "argus_pat_…",
|
|
73
90
|
// OPTIONAL — the timezone that decides when a check-by date becomes
|
|
74
|
-
// "today". Unset =
|
|
75
|
-
//
|
|
76
|
-
"ARGUS_TZ": "Asia/Seoul"
|
|
91
|
+
// "today". Unset = your machine's local timezone (usually right).
|
|
92
|
+
// Set it only if your machine's clock zone isn't where you live:
|
|
93
|
+
"ARGUS_TZ": "Asia/Seoul",
|
|
94
|
+
// OPTIONAL — opt in to anonymous usage telemetry (OFF by default). Sends
|
|
95
|
+
// a random install id + which tool ran + version/platform; never your
|
|
96
|
+
// decisions or token. Honors DO_NOT_TRACK. See SECURITY.md → Telemetry.
|
|
97
|
+
"ARGUS_TELEMETRY": "1"
|
|
77
98
|
// "ARGUS_API_URL": "https://argus.voyage" // override only for self-host
|
|
78
99
|
}
|
|
79
100
|
}
|
|
@@ -81,42 +102,107 @@ Or add to your host's MCP config:
|
|
|
81
102
|
}
|
|
82
103
|
```
|
|
83
104
|
|
|
105
|
+
**Claude Desktop** does **not** expand `${...}` variables — a literal
|
|
106
|
+
`${CLAUDE_PROJECT_DIR}` would fail on every call (Argus names this error when
|
|
107
|
+
it happens). Either omit `ARGUS_DIR` (→ `~/.argus`) or use an absolute path:
|
|
108
|
+
|
|
109
|
+
```jsonc
|
|
110
|
+
// macOS
|
|
111
|
+
"env": { "ARGUS_DIR": "/Users/you/.argus" }
|
|
112
|
+
// Windows — also note the command form below
|
|
113
|
+
"env": { "ARGUS_DIR": "C:\\Users\\you\\.argus" }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Windows** hosts often can't launch bare `npx` (it's `npx.cmd`). If the server
|
|
117
|
+
fails to start, use:
|
|
118
|
+
|
|
119
|
+
```jsonc
|
|
120
|
+
{
|
|
121
|
+
"command": "cmd",
|
|
122
|
+
"args": ["/c", "npx", "-y", "argus-decision-mcp"]
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
84
126
|
> `argus_dir` is **optional** on every tool: omit it and it resolves from
|
|
85
|
-
> `ARGUS_DIR`. A per-call `argus_dir` still
|
|
86
|
-
> even when env-variable interpolation
|
|
127
|
+
> `ARGUS_DIR`, then falls back to `~/.argus`. A per-call `argus_dir` still
|
|
128
|
+
> wins — so Argus works on any host even when env-variable interpolation
|
|
129
|
+
> doesn't.
|
|
130
|
+
|
|
131
|
+
## Your first receipt (2 minutes)
|
|
132
|
+
|
|
133
|
+
You never call these tools by name. Once the server is connected, you just
|
|
134
|
+
talk to your AI in plain language and it calls the right tool for you. One
|
|
135
|
+
full loop — from saving a prediction to recording what reality did — looks like
|
|
136
|
+
this:
|
|
137
|
+
|
|
138
|
+
**1 · Save a prediction** — before you commit to a decision, tell your AI:
|
|
139
|
+
|
|
140
|
+
> **You:** "I'm shipping the new onboarding flow next week. Save this prediction:
|
|
141
|
+
> signups go up at least 10% by the end of the month."
|
|
142
|
+
|
|
143
|
+
Argus records your predicate and a check-by date. Nothing is scored — it's a
|
|
144
|
+
prediction against reality, not a grade from the model.
|
|
145
|
+
|
|
146
|
+
**2 · Update a fact** *(optional, any time before the date)* — if a fact
|
|
147
|
+
your decision leaned on might have moved:
|
|
148
|
+
|
|
149
|
+
> **You:** "Update the signup number behind my onboarding decision. Has it
|
|
150
|
+
> changed since I saved the prediction?"
|
|
151
|
+
|
|
152
|
+
Argus compares the new number to the baseline and tells you if it drifted. It
|
|
153
|
+
returns the handle — whether to revisit the decision stays your call.
|
|
154
|
+
|
|
155
|
+
**3 · Record the result on the check-by date** — when the date arrives:
|
|
156
|
+
|
|
157
|
+
> **You:** "Record the result of my onboarding prediction. Signups went up 14%."
|
|
158
|
+
|
|
159
|
+
Argus prints the **Judgment Receipt** at the top of this page — with
|
|
160
|
+
`AI VERDICT … NONE`. You made the call; the record contains what reality did,
|
|
161
|
+
not a model's grade.
|
|
162
|
+
|
|
163
|
+
That's the whole spine. Everything below is detail on top of these three steps.
|
|
164
|
+
|
|
165
|
+
## The everyday loop
|
|
166
|
+
|
|
167
|
+
Argus now exposes six tools named for the job they do. You do not initialize
|
|
168
|
+
it first, choose a ritual, or learn its internal state machine. The first useful
|
|
169
|
+
call creates the local record automatically.
|
|
170
|
+
|
|
171
|
+
For most decisions the loop is simply:
|
|
172
|
+
|
|
173
|
+
1. **`argus_capture`** — capture a decision's premises and open questions, in your own words.
|
|
174
|
+
2. **`argus_predict`** — make a falsifiable prediction and the date reality can answer it.
|
|
175
|
+
3. **`argus_check_in`** — see only what needs attention now.
|
|
176
|
+
4. **`argus_resolve`** — record what actually happened, without a score or verdict.
|
|
87
177
|
|
|
88
|
-
|
|
178
|
+
`argus_patterns` reads what is already on record — past decisions and how often
|
|
179
|
+
your predictions held. `argus_settings` handles the few preferences and
|
|
180
|
+
account-sync controls a user may need.
|
|
89
181
|
|
|
90
182
|
| Tool | What it does |
|
|
91
183
|
|------|--------------|
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `argus_amend` | Changes the predicate or check-by date **before** reality answers — a course change, not an erasure (the original stays on the append-only ledger). Hard-errors once the decision is settled. |
|
|
99
|
-
| `argus_dismiss` | Closes a decision **without settling** — it became irrelevant, was decided elsewhere, or you changed your mind. No verdict is recorded; terminal, not reopened. |
|
|
100
|
-
| `argus_check_in` | Returns contracts past their check-by date **and premise facts due for a re-check** (the same fact under several decisions is one re-check). If nothing is due, it says so and stops — it doesn't manufacture engagement. |
|
|
101
|
-
| `argus_sync` | Pulls your account receipts into the terminal (live judgments + what's due) so you can settle here. Seals push automatically; this is the read side. Requires `ARGUS_TOKEN`. |
|
|
102
|
-
| `argus_recall` | Reads your own history: a receipt, the open contracts, or a sample-size-caveated track record (never a tier or score). |
|
|
103
|
-
| `argus_init` / `argus_config` | Initialize the `.argus` directory; read/write non-spine settings. |
|
|
184
|
+
| `argus_capture` | Captures one decision's premises and open questions in the user's own words, without deciding for you. Its actions add assumptions and open questions, record your answer, update an external fact, change an untested prediction, or close a decision that no longer needs an outcome. |
|
|
185
|
+
| `argus_predict` | Makes a falsifiable prediction (`predicate` + `check_by`) in the user's words. An Argus-drafted line is marked honestly and shown as a one-tap draft to keep, reword, or skip. |
|
|
186
|
+
| `argus_check_in` | Shows only predictions past their check date, external facts due for an update, and open questions due for reconsideration. If nothing needs attention, it stops. |
|
|
187
|
+
| `argus_resolve` | Records what actually happened and issues the Judgment Receipt. Reality supplies the outcome; Argus does not grade it. |
|
|
188
|
+
| `argus_patterns` | Reads active decisions, all records, one Judgment Receipt, one decision's context, or the accumulated frequency of outcomes. Read-only. |
|
|
189
|
+
| `argus_settings` | Reads or updates language, quiet reminders, opt-in premise sync, and explicit account sync. Initialization is automatic. |
|
|
104
190
|
|
|
105
191
|
## Living premises
|
|
106
192
|
|
|
107
193
|
The receipt's sharpest line — `THE UNVERIFIED ASSUMPTION` — used to be written
|
|
108
|
-
once
|
|
194
|
+
once when a prediction was saved and then go dead. Now it's a **tracked object**: name the premises
|
|
109
195
|
a decision rests on, correct the ones the model got wrong (your edit is part of
|
|
110
196
|
the record), and re-check the load-bearing external facts against reality while
|
|
111
197
|
the bet is still open. When a rate hike breaks the premise under three of your
|
|
112
198
|
decisions, that's **one** re-check, not three.
|
|
113
199
|
|
|
114
200
|
Honest limits, stated up front: an MCP server is passive — nothing wakes it
|
|
115
|
-
between
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
201
|
+
between saving a prediction and recording its result. Every tool response can
|
|
202
|
+
carry a quiet `due_note`, `argus_check_in` reports due items, and the single
|
|
203
|
+
`argus://attention` resource lets capable hosts auto-inject what needs attention. No
|
|
204
|
+
separate prompt ritual is advertised. Anything more periodic (cron, reminders)
|
|
205
|
+
belongs to your host or habits, not this server.
|
|
120
206
|
A premise that never gets re-checked shows up honestly as `never re-checked` —
|
|
121
207
|
Argus does not pretend liveness.
|
|
122
208
|
|
|
@@ -124,12 +210,20 @@ Argus does not pretend liveness.
|
|
|
124
210
|
|
|
125
211
|
Everything is local, under `.argus/` in your project (gitignored by default) —
|
|
126
212
|
an append-only `ledger.jsonl` **you own**: plain JSON lines, no lock-in,
|
|
127
|
-
receipts render to shareable text. No telemetry
|
|
128
|
-
Argus
|
|
129
|
-
`ARGUS_TOKEN`, a
|
|
130
|
-
so it can email you at its check-by date.
|
|
131
|
-
|
|
132
|
-
|
|
213
|
+
receipts render to shareable text. **No telemetry by default.** The only network
|
|
214
|
+
call Argus makes out of the box is the opt-in account sync: if — and only if —
|
|
215
|
+
you set `ARGUS_TOKEN`, a saved prediction or recorded result is POSTed to your own Argus
|
|
216
|
+
account so it can email you at its check-by date. Separately, you can opt in to
|
|
217
|
+
**anonymous** usage telemetry with `ARGUS_TELEMETRY=1` (a random install id +
|
|
218
|
+
which tool ran + version/platform, never your decisions or token; honors
|
|
219
|
+
`DO_NOT_TRACK`) — see SECURITY.md. **Premise data stays on your machine
|
|
220
|
+
by default** — it is not part of the sync payload. There is exactly one switch
|
|
221
|
+
that changes this: `argus_settings` with `action:"update"` and
|
|
222
|
+
`premise_sync:true` (off unless you set it)
|
|
223
|
+
sends a saved decision's *monitored* premises along, so your account's
|
|
224
|
+
autonomous premise-watch can re-check them against reality and email you when
|
|
225
|
+
one materially moves. Unset the token and Argus never touches the network.
|
|
226
|
+
See [SECURITY.md](SECURITY.md).
|
|
133
227
|
|
|
134
228
|
## Measured
|
|
135
229
|
|
package/SECURITY.md
CHANGED
|
@@ -2,14 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Data & privacy
|
|
4
4
|
|
|
5
|
-
- **All data is local.** Argus writes only under the `.argus/`
|
|
6
|
-
point it at: an append-only `ledger/ledger.jsonl`,
|
|
7
|
-
`sessions/{id}/` files, and a `config.yaml`. Nothing leaves the
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
- **All data is local by default.** Argus writes only under the `.argus/`
|
|
6
|
+
directory you point it at: an append-only `ledger/ledger.jsonl`,
|
|
7
|
+
per-decision `sessions/{id}/` files, and a `config.yaml`. Nothing leaves the
|
|
8
|
+
machine unless you explicitly set `ARGUS_TOKEN` for account sync (or opt in to
|
|
9
|
+
anonymous telemetry with `ARGUS_TELEMETRY=1`; see below).
|
|
10
|
+
- **No telemetry by default.** With no `ARGUS_TOKEN` and no `ARGUS_TELEMETRY=1`,
|
|
11
|
+
the server makes no network calls. Telemetry is strictly **opt-in**: set
|
|
12
|
+
`ARGUS_TELEMETRY=1` and it sends an anonymous, content-free ping — a random,
|
|
13
|
+
machine-local install id (`~/.argus/.telemetry-id`, not tied to your account
|
|
14
|
+
or token, regenerable by deleting the file), which of the built-in tools ran
|
|
15
|
+
and whether it succeeded, plus the package version, coarse OS platform, and
|
|
16
|
+
Node major. It **never** sends decision content, titles, predicates, file
|
|
17
|
+
paths, `argus_dir`, or the account token. `DO_NOT_TRACK=1` disables it even if
|
|
18
|
+
the flag is set, and the sink stores no `user_id` (nothing to identify).
|
|
19
|
+
Telemetry rows are **deleted automatically after 90 days** (a daily database
|
|
20
|
+
job — retention is enforced by the database itself, not by policy prose). The
|
|
21
|
+
runtime dependency surface is intentionally small and visible in
|
|
22
|
+
`package.json`: MCP SDK, schema/config helpers, and document parsers.
|
|
23
|
+
`npm audit --omit=dev` should stay clean before publishing.
|
|
24
|
+
- **Private by default.** On first use Argus adds `sessions/`, `ledger/`,
|
|
25
|
+
`config.yaml`, and `.bound` to `.argus/.gitignore` so your decisions and
|
|
26
|
+
local settings are never committed accidentally.
|
|
13
27
|
|
|
14
28
|
## Threat model
|
|
15
29
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { writeStoredAccountCredential, } from './account-credentials.js';
|
|
5
|
+
const DEFAULT_API_URL = 'https://argus.voyage';
|
|
6
|
+
const CONNECT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
7
|
+
function apiBase(value) {
|
|
8
|
+
const url = new URL(value.replace(/\/+$/, ''));
|
|
9
|
+
const local = url.hostname === '127.0.0.1' || url.hostname === 'localhost' || url.hostname === '::1';
|
|
10
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && local)) {
|
|
11
|
+
throw new Error('ARGUS_API_URL must use HTTPS (HTTP is allowed only for localhost).');
|
|
12
|
+
}
|
|
13
|
+
return url.toString().replace(/\/+$/, '');
|
|
14
|
+
}
|
|
15
|
+
function base64url(bytes = 32) {
|
|
16
|
+
return randomBytes(bytes).toString('base64url');
|
|
17
|
+
}
|
|
18
|
+
function challenge(verifier) {
|
|
19
|
+
return createHash('sha256').update(verifier).digest('base64url');
|
|
20
|
+
}
|
|
21
|
+
async function defaultOpenExternal(url) {
|
|
22
|
+
const command = process.platform === 'win32' ? 'explorer.exe' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
23
|
+
try {
|
|
24
|
+
const child = spawn(command, [url], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
25
|
+
child.unref();
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function listen(server) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
server.once('error', reject);
|
|
35
|
+
server.listen(0, '127.0.0.1', () => resolve(server.address().port));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function close(server) {
|
|
39
|
+
return new Promise((resolve) => server.close(() => resolve()));
|
|
40
|
+
}
|
|
41
|
+
function waitForCode(server, expectedState) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const timeout = setTimeout(() => reject(new Error('Account connection timed out.')), CONNECT_TIMEOUT_MS);
|
|
44
|
+
server.on('request', (request, response) => {
|
|
45
|
+
const url = new URL(request.url || '/', 'http://127.0.0.1');
|
|
46
|
+
if (url.pathname !== '/callback') {
|
|
47
|
+
response.writeHead(404).end('Not found');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const code = url.searchParams.get('code');
|
|
51
|
+
const state = url.searchParams.get('state');
|
|
52
|
+
if (!code || state !== expectedState) {
|
|
53
|
+
response.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' }).end('Invalid or expired Argus connection.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
response.writeHead(200, {
|
|
58
|
+
'content-type': 'text/html; charset=utf-8',
|
|
59
|
+
'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'",
|
|
60
|
+
'cache-control': 'no-store',
|
|
61
|
+
}).end('<!doctype html><meta charset="utf-8"><title>Argus connected</title><style>body{font:16px system-ui;max-width:38rem;margin:15vh auto;padding:2rem;color:#202020}h1{font-size:1.4rem}</style><h1>Argus account connected</h1><p>You can close this window and return to your terminal.</p>');
|
|
62
|
+
resolve(code);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function requestJson(fetchImpl, url, body) {
|
|
67
|
+
const response = await fetchImpl(url, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify(body),
|
|
71
|
+
});
|
|
72
|
+
let json = {};
|
|
73
|
+
try {
|
|
74
|
+
json = await response.json();
|
|
75
|
+
}
|
|
76
|
+
catch { /* surfaced as protocol failure below */ }
|
|
77
|
+
return { response, json };
|
|
78
|
+
}
|
|
79
|
+
function tokenFrom(json) {
|
|
80
|
+
if (typeof json.access_token !== 'string' || !json.access_token.startsWith('argus_pat_'))
|
|
81
|
+
throw new Error('The account returned an invalid credential.');
|
|
82
|
+
if (json.token_type !== 'Bearer' || typeof json.scope !== 'string' || typeof json.expires_at !== 'string')
|
|
83
|
+
throw new Error('The account returned an incomplete credential.');
|
|
84
|
+
return json;
|
|
85
|
+
}
|
|
86
|
+
function persist(token, base, credentialFile) {
|
|
87
|
+
const stored = {
|
|
88
|
+
version: 1,
|
|
89
|
+
access_token: token.access_token,
|
|
90
|
+
token_type: 'Bearer',
|
|
91
|
+
scope: token.scope,
|
|
92
|
+
expires_at: token.expires_at,
|
|
93
|
+
api_url: base,
|
|
94
|
+
connected_at: new Date().toISOString(),
|
|
95
|
+
};
|
|
96
|
+
writeStoredAccountCredential(stored, credentialFile);
|
|
97
|
+
return stored;
|
|
98
|
+
}
|
|
99
|
+
async function connectWithBrowser(options, base) {
|
|
100
|
+
const verifier = base64url(48);
|
|
101
|
+
const state = base64url(24);
|
|
102
|
+
const server = createServer();
|
|
103
|
+
const port = await listen(server);
|
|
104
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
105
|
+
const codePromise = waitForCode(server, state);
|
|
106
|
+
const authorize = new URL(`${base}/en/auth/callback/mcp-connect`);
|
|
107
|
+
authorize.searchParams.set('response_type', 'code');
|
|
108
|
+
authorize.searchParams.set('redirect_uri', redirectUri);
|
|
109
|
+
authorize.searchParams.set('state', state);
|
|
110
|
+
authorize.searchParams.set('code_challenge', challenge(verifier));
|
|
111
|
+
authorize.searchParams.set('code_challenge_method', 'S256');
|
|
112
|
+
authorize.searchParams.set('client_name', options.clientName || 'Argus MCP');
|
|
113
|
+
options.writeLine('Opening Argus in your browser to approve this device…');
|
|
114
|
+
const opened = await options.openExternal(authorize.toString());
|
|
115
|
+
if (!opened)
|
|
116
|
+
options.writeLine(`Open this URL in a browser:\n${authorize.toString()}`);
|
|
117
|
+
try {
|
|
118
|
+
const code = await codePromise;
|
|
119
|
+
const { response, json } = await requestJson(options.fetchImpl, `${base}/api/mcp/oauth/token`, {
|
|
120
|
+
grant_type: 'authorization_code',
|
|
121
|
+
code,
|
|
122
|
+
code_verifier: verifier,
|
|
123
|
+
redirect_uri: redirectUri,
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok)
|
|
126
|
+
throw new Error(`Account token exchange failed (${String(json.error || response.status)}).`);
|
|
127
|
+
return persist(tokenFrom(json), base, options.credentialFile);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await close(server);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function connectWithDeviceCode(options, base) {
|
|
134
|
+
const { response, json } = await requestJson(options.fetchImpl, `${base}/api/mcp/oauth/device`, { client_name: options.clientName || 'Argus MCP' });
|
|
135
|
+
if (!response.ok)
|
|
136
|
+
throw new Error(`Could not start device authorization (${String(json.error || response.status)}).`);
|
|
137
|
+
if (typeof json.device_code !== 'string' || typeof json.user_code !== 'string' || typeof json.verification_uri !== 'string') {
|
|
138
|
+
throw new Error('The account returned an invalid device authorization.');
|
|
139
|
+
}
|
|
140
|
+
let interval = typeof json.interval === 'number' ? Math.max(1, json.interval) : 5;
|
|
141
|
+
const expiresIn = typeof json.expires_in === 'number' ? json.expires_in : 600;
|
|
142
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
143
|
+
options.writeLine(`Open ${String(json.verification_uri)} and enter code: ${json.user_code}`);
|
|
144
|
+
while (Date.now() < deadline) {
|
|
145
|
+
await options.sleep(interval * 1000);
|
|
146
|
+
const poll = await requestJson(options.fetchImpl, `${base}/api/mcp/oauth/token`, {
|
|
147
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
148
|
+
device_code: json.device_code,
|
|
149
|
+
});
|
|
150
|
+
if (poll.response.ok)
|
|
151
|
+
return persist(tokenFrom(poll.json), base, options.credentialFile);
|
|
152
|
+
if (poll.json.error === 'authorization_pending')
|
|
153
|
+
continue;
|
|
154
|
+
if (poll.json.error === 'slow_down') {
|
|
155
|
+
interval += 5;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (poll.json.error === 'access_denied')
|
|
159
|
+
throw new Error('Account connection was denied.');
|
|
160
|
+
if (poll.json.error === 'expired_token')
|
|
161
|
+
break;
|
|
162
|
+
throw new Error(`Device authorization failed (${String(poll.json.error || poll.response.status)}).`);
|
|
163
|
+
}
|
|
164
|
+
throw new Error('Device authorization expired. Start the connection again.');
|
|
165
|
+
}
|
|
166
|
+
export async function connectAccount(options = {}) {
|
|
167
|
+
const base = apiBase(options.apiUrl || process.env.ARGUS_API_URL || DEFAULT_API_URL);
|
|
168
|
+
const shared = {
|
|
169
|
+
...options,
|
|
170
|
+
fetchImpl: options.fetchImpl || fetch,
|
|
171
|
+
openExternal: options.openExternal || defaultOpenExternal,
|
|
172
|
+
writeLine: options.writeLine || ((line) => process.stdout.write(`${line}\n`)),
|
|
173
|
+
sleep: options.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
174
|
+
};
|
|
175
|
+
const credential = options.headless
|
|
176
|
+
? await connectWithDeviceCode(shared, base)
|
|
177
|
+
: await connectWithBrowser(shared, base);
|
|
178
|
+
shared.writeLine(`Connected. Credential stored locally; expires ${credential.expires_at.slice(0, 10)}.`);
|
|
179
|
+
return credential;
|
|
180
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function defaultCredentialPath() {
|
|
5
|
+
const explicit = process.env.ARGUS_ACCOUNT_FILE?.trim();
|
|
6
|
+
if (explicit)
|
|
7
|
+
return path.resolve(explicit);
|
|
8
|
+
const argusDir = process.env.ARGUS_DIR?.trim();
|
|
9
|
+
if (argusDir)
|
|
10
|
+
return path.join(path.resolve(argusDir), 'account-credentials.json');
|
|
11
|
+
if (process.platform === 'win32') {
|
|
12
|
+
const root = process.env.LOCALAPPDATA?.trim() || path.join(os.homedir(), 'AppData', 'Local');
|
|
13
|
+
return path.join(root, 'Argus', 'account-credentials.json');
|
|
14
|
+
}
|
|
15
|
+
if (process.platform === 'darwin') {
|
|
16
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Argus', 'account-credentials.json');
|
|
17
|
+
}
|
|
18
|
+
const root = process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config');
|
|
19
|
+
return path.join(root, 'argus', 'account-credentials.json');
|
|
20
|
+
}
|
|
21
|
+
export function accountCredentialPath() {
|
|
22
|
+
return defaultCredentialPath();
|
|
23
|
+
}
|
|
24
|
+
export function readStoredAccountCredential(file = defaultCredentialPath()) {
|
|
25
|
+
// Unit tests must opt into an isolated temp location. Never probe a
|
|
26
|
+
// developer's actual config directory merely because a sync helper ran.
|
|
27
|
+
if (process.env.NODE_ENV === 'test' && !process.env.ARGUS_ACCOUNT_FILE && !process.env.ARGUS_DIR && file === defaultCredentialPath()) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
32
|
+
if (parsed.version !== 1 || parsed.token_type !== 'Bearer')
|
|
33
|
+
return null;
|
|
34
|
+
if (typeof parsed.access_token !== 'string' || !parsed.access_token.startsWith('argus_pat_'))
|
|
35
|
+
return null;
|
|
36
|
+
if (typeof parsed.expires_at !== 'string' || Date.parse(parsed.expires_at) <= Date.now())
|
|
37
|
+
return null;
|
|
38
|
+
if (typeof parsed.api_url !== 'string' || typeof parsed.scope !== 'string' || typeof parsed.connected_at !== 'string')
|
|
39
|
+
return null;
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function writeStoredAccountCredential(credential, file = defaultCredentialPath()) {
|
|
47
|
+
const dir = path.dirname(file);
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
49
|
+
const temp = path.join(dir, `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(temp, `${JSON.stringify(credential, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
52
|
+
fs.renameSync(temp, file);
|
|
53
|
+
try {
|
|
54
|
+
fs.chmodSync(file, 0o600);
|
|
55
|
+
}
|
|
56
|
+
catch { /* Windows ACLs do not use POSIX modes. */ }
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
try {
|
|
60
|
+
fs.unlinkSync(temp);
|
|
61
|
+
}
|
|
62
|
+
catch { /* rename already removed it */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Manual ARGUS_TOKEN remains the advanced/CI override. */
|
|
66
|
+
export function resolveAccountToken() {
|
|
67
|
+
const manual = process.env.ARGUS_TOKEN?.trim();
|
|
68
|
+
if (manual)
|
|
69
|
+
return manual;
|
|
70
|
+
return readStoredAccountCredential()?.access_token ?? '';
|
|
71
|
+
}
|
|
72
|
+
export function resolveAccountApiUrl() {
|
|
73
|
+
const manual = process.env.ARGUS_API_URL?.trim();
|
|
74
|
+
if (manual)
|
|
75
|
+
return manual;
|
|
76
|
+
return readStoredAccountCredential()?.api_url ?? 'https://argus.voyage';
|
|
77
|
+
}
|
|
78
|
+
export function disconnectStoredAccount(file = defaultCredentialPath()) {
|
|
79
|
+
try {
|
|
80
|
+
fs.unlinkSync(file);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return error.code === 'ENOENT';
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
import { createServer } from './server.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
async function main() {
|
|
5
|
+
if (process.argv[2] === 'connect') {
|
|
6
|
+
const { connectAccount } = await import('./a0/account-connect.js');
|
|
7
|
+
await connectAccount({ headless: process.argv.includes('--headless') });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (process.argv[2] === 'disconnect') {
|
|
11
|
+
const { disconnectStoredAccount } = await import('./a0/account-credentials.js');
|
|
12
|
+
disconnectStoredAccount();
|
|
13
|
+
process.stdout.write('Local Argus account credential removed. Revoke the device in Settings to invalidate its server token.\n');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (process.argv[2] === 'capture-scan') {
|
|
17
|
+
const { runCaptureCli } = await import('./v2/capture-cli.js');
|
|
18
|
+
await runCaptureCli(process.argv.slice(3));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (process.argv[2] === 'capture-status') {
|
|
22
|
+
const { runCaptureStatusCli } = await import('./v2/capture-cli.js');
|
|
23
|
+
runCaptureStatusCli(process.argv.slice(3));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (process.argv[2] === 'capture-purge') {
|
|
27
|
+
const { runCapturePurgeCli } = await import('./v2/capture-cli.js');
|
|
28
|
+
runCapturePurgeCli(process.argv.slice(3));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (['archive-export', 'archive-restore', 'local-purge'].includes(process.argv[2] ?? '')) {
|
|
32
|
+
const { runLifecycleCli } = await import('./v2/lifecycle-cli.js');
|
|
33
|
+
runLifecycleCli(process.argv[2], process.argv.slice(3));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
5
36
|
const server = await createServer();
|
|
6
37
|
const transport = new StdioServerTransport();
|
|
7
38
|
await server.connect(transport);
|
|
@@ -10,4 +41,7 @@ async function main() {
|
|
|
10
41
|
process.exit(0);
|
|
11
42
|
});
|
|
12
43
|
}
|
|
13
|
-
main().catch(
|
|
44
|
+
main().catch((error) => {
|
|
45
|
+
console.error(error);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
});
|