@dreamforgestudiollc/emet 0.0.0-stage → 0.2.6
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/.env.example +189 -0
- package/CHANGELOG.md +360 -0
- package/CHARTER.md +137 -0
- package/LICENSE +22 -0
- package/README.md +868 -2
- package/docs/AUTHORSHIP.md +26 -0
- package/docs/BENCHMARKS.md +261 -0
- package/docs/BOOTSTRAP.md +127 -0
- package/docs/BOTS.md +83 -0
- package/docs/COVERAGE.md +64 -0
- package/docs/DEPLOY.md +251 -0
- package/docs/DISCIPLINES.md +29 -0
- package/docs/GUARDRAILS-AND-MONORAILS.md +108 -0
- package/docs/GUIDED-SETUP.md +301 -0
- package/docs/PERMISSIONS.md +121 -0
- package/docs/ROADMAP.md +62 -0
- package/docs/SETUP.md +265 -0
- package/docs/SUBKEYS.md +59 -0
- package/gems/artifact-index.json +12 -0
- package/gems/bot-team-backup/floor.json +15 -0
- package/gems/bot-team-backup/gem.json +43 -0
- package/gems/bot-team-backup/guides/BOT_TEAM_BACKUP.md +27 -0
- package/gems/bot-team-backup/guides/BOT_TEAM_RECOVERY.md +23 -0
- package/gems/fieldnote/floor.json +14 -0
- package/gems/fieldnote/gem.json +70 -0
- package/gems/fieldnote/runes/fieldnote.js +4 -0
- package/gems/lean-six-sigma/CHANGELOG.md +10 -0
- package/gems/lean-six-sigma/README.md +92 -0
- package/gems/lean-six-sigma/floor.json +31 -0
- package/gems/lean-six-sigma/gem.json +1296 -0
- package/gems/lean-six-sigma/guides/ASSUMPTIONS.md +45 -0
- package/gems/lean-six-sigma/guides/BELTS.md +52 -0
- package/gems/lean-six-sigma/guides/DMADV.md +56 -0
- package/gems/lean-six-sigma/guides/DMAIC.md +127 -0
- package/gems/lean-six-sigma/guides/FORMULAS.md +330 -0
- package/gems/lean-six-sigma/guides/LEAN.md +97 -0
- package/gems/lean-six-sigma/guides/TEMPLATES.md +279 -0
- package/gems/lean-six-sigma/runes/lss_capability.js +132 -0
- package/gems/lean-six-sigma/runes/lss_control_limits.js +240 -0
- package/gems/lean-six-sigma/runes/lss_dpmo_sigma.js +81 -0
- package/gems/lean-six-sigma/runes/lss_hypothesis.js +298 -0
- package/gems/lean-six-sigma/runes/lss_sample_size.js +81 -0
- package/gems/neurendipity/floor.json +65 -0
- package/gems/neurendipity/gem.json +42 -0
- package/gems/neurendipity/guides/AUTISTIC_BURNOUT.md +81 -0
- package/gems/neurendipity/guides/NEURODIVERGENT_SUPPORT.md +287 -0
- package/guides/README.md +3 -0
- package/package.json +76 -4
- package/server.json +79 -0
- package/src/aleph.js +395 -0
- package/src/annotations.js +170 -0
- package/src/approve-throttle.js +54 -0
- package/src/atlas-role.js +58 -0
- package/src/attribution.js +555 -0
- package/src/conformance.js +688 -0
- package/src/content_analyzer.js +450 -0
- package/src/database.js +812 -0
- package/src/disciplines.js +26 -0
- package/src/dispatch.js +382 -0
- package/src/env.js +97 -0
- package/src/floor.js +228 -0
- package/src/gaps.js +509 -0
- package/src/gem-check.js +1359 -0
- package/src/guides.js +75 -0
- package/src/host-reach.js +23 -0
- package/src/http-remote.js +313 -0
- package/src/index.js +397 -0
- package/src/invites.js +167 -0
- package/src/layers.js +449 -0
- package/src/lessons-to-checks.js +366 -0
- package/src/logger.js +300 -0
- package/src/metadata.js +61 -0
- package/src/module-path.js +32 -0
- package/src/mongo-connect.js +91 -0
- package/src/named-items.js +208 -0
- package/src/oauth-provider.js +838 -0
- package/src/owner-approvals.js +281 -0
- package/src/persons.js +136 -0
- package/src/provenance.js +257 -0
- package/src/redact.js +237 -0
- package/src/request-log.js +124 -0
- package/src/rune-dispatch.js +360 -0
- package/src/rune-registry.js +71 -0
- package/src/rune-scope.js +42 -0
- package/src/scopes.js +100 -0
- package/src/security-headers.js +26 -0
- package/src/session-graph.js +947 -0
- package/src/session.js +1097 -0
- package/src/setup.js +1580 -0
- package/src/socket.js +545 -0
- package/src/startup-pages.js +371 -0
- package/src/startup-payload.js +129 -0
- package/src/subkey-store.js +357 -0
- package/src/subkeys.js +947 -0
- package/src/tools.js +4508 -0
- package/src/transcript-session.js +106 -0
- package/src/validation.js +1187 -0
- package/src/version.js +2 -0
- package/templates/BOARD.md +72 -0
- package/templates/BOTHANDOFF.md +57 -0
- package/templates/CHARACTER.md +30 -0
- package/templates/HANDOFF.md +63 -0
- package/templates/IDENTITY.md +29 -0
- package/templates/INDEX.md +82 -0
- package/templates/INSTALL.md +29 -0
- package/templates/PROCEDURE.md +39 -0
- package/templates/ROSTER.md +43 -0
- package/templates/SEED.md +65 -0
- package/templates/STAGING.md +37 -0
- package/templates/STANDARD.md +99 -0
- package/templates/THREAD.md +43 -0
- package/templates/USER.md +32 -0
- package/verification/load-gate/corpus.json +1765 -0
- package/verification/load-gate/load-gate.mjs +1246 -0
package/docs/DEPLOY.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Deploying EMET as a remote MCP server
|
|
2
|
+
|
|
3
|
+
This deploys `src/http-remote.js` as an OAuth-protected HTTPS endpoint, so hosts that cannot
|
|
4
|
+
run a local process — phones, tablets, web clients, third-party agents — reach the same
|
|
5
|
+
memory store as your desktop.
|
|
6
|
+
|
|
7
|
+
The memory logic is identical to the local server. Only the transport differs.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
Finish **Stage 1** of [SETUP.md](SETUP.md) first. You need a `mongodb+srv://` URI with a
|
|
12
|
+
scoped user, and Atlas Network Access set to `0.0.0.0/0` — a hosted platform's egress IP is
|
|
13
|
+
dynamic, so it cannot be allowlisted. Security rests on the scoped user plus the OAuth
|
|
14
|
+
passphrase; both are required.
|
|
15
|
+
|
|
16
|
+
## Environment variables
|
|
17
|
+
|
|
18
|
+
| Variable | Required | Purpose |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `MONGODB_URI` | yes | Atlas connection string, scoped user. |
|
|
21
|
+
| `MONGODB_DB` | yes | Database name. Must match your other hosts, or they will not see each other's writes. |
|
|
22
|
+
| `PUBLIC_URL` | yes | The public **https** base URL. This is the OAuth issuer and must match the deployed URL exactly. |
|
|
23
|
+
| `EMET_OAUTH_PASSPHRASE` | yes | Typed once per device to authorize it. Make it strong — it is the only thing between the internet and your memory. |
|
|
24
|
+
| `EMET_SOURCE_TAG` | optional | Default tag for a write that passes no `source`. Leave it unset on a server several hosts share (see below); set it only when one host uses this deployment. |
|
|
25
|
+
| `EMET_SUBKEYS` | when more than one bot shares the owner login | `1` turns on delegated client sub-keys. On this kind of host, set it in the platform's variables. Setup refuses to write a pepper into the container `.env`. Restart or redeploy so the tools and `/owner/approvals` appear together. See [SUBKEYS.md](SUBKEYS.md). |
|
|
26
|
+
| `EMET_SUBKEY_PEPPER` | with that flag | Server secret, at least 32 bytes, generated by the owner on a hosted deploy (`openssl rand -hex 32`). Not stored in the database. |
|
|
27
|
+
| `EMET_SUBKEY_PEPPER_VERSION` | with that flag | `1` until the pepper is rotated. Set it with the other two, then redeploy. |
|
|
28
|
+
| `EMET_TIMEZONE` | optional | IANA time zone that session ids are dated in, e.g. `America/Chicago`. Default `America/New_York`. An unknown name falls back to the default and `emet_session_open` says so. |
|
|
29
|
+
| `EMET_CLOSE_GRACE_MINUTES` | optional | Minutes after a complete close in which a session still takes its one closing exchange (the reply sent after the close). Default `5`; `0` turns the window off; at most `60` (a larger value is capped at 60). A negative or non-numeric value is not used: the server logs a warning and uses `5`. |
|
|
30
|
+
| `EMET_GAP_BASELINE` | optional | ISO date (or Unix seconds) from which the record is held to the current rules. The startup line then reports gaps since that date beside the lifetime figure. |
|
|
31
|
+
| `PORT` | auto | Injected by the platform. |
|
|
32
|
+
| `TRUST_PROXY` | not read | The server trusts exactly one proxy hop (Railway's edge). TRUST_PROXY is not read. The `/mcp` body limit is 256kb. |
|
|
33
|
+
|
|
34
|
+
## Checklist before 0.2.3
|
|
35
|
+
|
|
36
|
+
Read this before you deploy the release that follows 0.2.2. It does not change a version number. It is the list of settings and client habits that will start failing if you leave them as they were.
|
|
37
|
+
|
|
38
|
+
- **`EMET_SOURCE_TAGS` must list every real host tag**, as an exact member. Tags named in this repository's example env, docs, and live-tag checks are: `cli`, `mobile`, `desktop-host`, `cloud-host`, `project-host`, `hub-bot` (the hub; do not also list it in `EMET_RECORD_DOCS`), `reviewer-bot`, `tester-bot`, `memory-bot`, `intake-bot`, and `research-bot`. Your install may use a shorter list. A tag that is not an exact member is refused, and the refusal does not print the list.
|
|
39
|
+
- When a new bot joins the roster, append its tag to the end of `EMET_SOURCE_TAGS` and keep every existing tag in order, before its first write. The standing rule, including `EMET_RECORD_DOCS` and the `/health` and `emet_status` check after the redeploy, is [BOTS.md](BOTS.md) §4.
|
|
40
|
+
- **`EMET_STRICT_LOGIN_TAG=1`** (off by default) makes a login's bound tag final: a write that declares a different `source` is refused, and so are `emet_setup_complete` and `emet_initialize` with a mismatched tag. Leave it off while bots share one login, because with it on every bot except the login's own would be refused. Turn it on only once each bot has its own login, for example a Member Access code bound to its tag.
|
|
41
|
+
- **A scheduled job that still sends a `cloud-scheduled-` prefix must switch to an exact registered tag.** `cloud-scheduled-intake` was retired on 2026-10-02. A prefix is not a tag. The weekly intake runs under `intake-bot`.
|
|
42
|
+
- **An HTTP client whose stored token has no source tag must sign in again.** Refresh of that token is refused. The new consent binds a registered tag. An empty registry has no members.
|
|
43
|
+
- **Caps already in force.** A client that used to send more is refused, and nothing is written:
|
|
44
|
+
- 20 new drops per New York day, in one bucket: the whole local process on stdio, one shared visitor bucket for HTTP with no signed-in client, the source tag bound at approval when there is one, or that OAuth client otherwise. A source the caller types does not move the bucket.
|
|
45
|
+
- HTTP request body: 256 KB.
|
|
46
|
+
- A transcript field: 32,000 characters. The whole transcript request: 256,000 characters.
|
|
47
|
+
- Tool calls: 300 per minute for that person and client, and a per-tool cap (120 for `recall` and `semantic_recall`, 100 for `query_layer`, 30 for `emet_status`, 60 for `save_to_layer`, 60 for every other tool).
|
|
48
|
+
- Client-metadata fetches: 60 per host per minute, 120 in total, 64 KB, 5 second timeout. The connection uses the public address already checked. A redirect is refused.
|
|
49
|
+
- A document edit may not leave it under half the size this session first saw, and not under half of its largest size in the last 24 hours.
|
|
50
|
+
- Recall ranks in the store before it keeps a window of at least 200 rows, or 20 times the page.
|
|
51
|
+
- The window for one closing exchange after a complete close is at most 60 minutes.
|
|
52
|
+
|
|
53
|
+
## Before you touch anything
|
|
54
|
+
|
|
55
|
+
**Set the environment variables BEFORE the first deploy, not after.** A server that boots once
|
|
56
|
+
with the wrong configuration creates collections, mints OAuth tokens, and generally leaves
|
|
57
|
+
residue you then have to reason about. Variables first, deploy second.
|
|
58
|
+
|
|
59
|
+
**Write down the current deployment id** if you are replacing a running service:
|
|
60
|
+
`railway status` prints it. Rolling back is Dashboard → Deployments → that entry → Redeploy.
|
|
61
|
+
|
|
62
|
+
**⚠ `railway variables` prints values in plaintext**, including your connection string and your
|
|
63
|
+
passphrase. Use `railway variables --kv` and read only the key names unless you specifically
|
|
64
|
+
need a value. Do not paste that output into a chat, an issue, or a screenshot. Redacting it
|
|
65
|
+
afterwards does not work reliably - table output wraps long values across lines, and a
|
|
66
|
+
credential split over two rows defeats the pattern you were counting on.
|
|
67
|
+
|
|
68
|
+
## Railway
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
npm i -g @railway/cli
|
|
72
|
+
railway login
|
|
73
|
+
railway init # from the repo root
|
|
74
|
+
railway up
|
|
75
|
+
railway variables --set MONGODB_URI=... --set MONGODB_DB=emet \
|
|
76
|
+
--set EMET_OAUTH_PASSPHRASE=... --set EMET_SOURCE_TAGS=mobile,desktop,cli
|
|
77
|
+
railway domain # get the public URL
|
|
78
|
+
railway variables --set PUBLIC_URL=https://<app>.up.railway.app
|
|
79
|
+
railway up # redeploy so PUBLIC_URL matches the live URL
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
One service serves every host, so register the hosts' tags with `EMET_SOURCE_TAGS` and leave
|
|
83
|
+
`EMET_SOURCE_TAG` unset: a default tag makes every host look the same (see `.env.example`).
|
|
84
|
+
Over HTTP, document, transcript, and memory writes use the tag bound when that host was
|
|
85
|
+
approved. A local stdio host still passes an exact registered tag as `source`.
|
|
86
|
+
|
|
87
|
+
**Tokens issued before source tags were bound.** An access token whose row has no `source_tag`
|
|
88
|
+
can still write when the caller names a tag that is an exact member of `EMET_SOURCE_TAGS`.
|
|
89
|
+
That is no wider than the registry: an empty registry has no members, and a tag that is only
|
|
90
|
+
a prefix match does not count. Refreshing such a token is refused with `invalid_grant` and a
|
|
91
|
+
message to sign in again, so the new consent binds a registered tag. The empty tag is not
|
|
92
|
+
copied onto the replacement. Those logins age out on their own as each access token expires
|
|
93
|
+
and the refresh is refused. After this deploy, a host whose writes are refused for a missing
|
|
94
|
+
source tag signs in once. A member code can carry a registered tag: the owner sets `source`
|
|
95
|
+
when creating the code, and redeem binds that tag.
|
|
96
|
+
|
|
97
|
+
**The monorail runs in enforce only** (2026-10-03). There is no mode setting: every gate refuses a
|
|
98
|
+
skipped step before anything is written, and the reply says how to pass. `EMET_SESSION_GRAPH` is
|
|
99
|
+
no longer read; remove it from the service's variables in the Railway dashboard. If the session
|
|
100
|
+
store cannot be read, a governed write that names a session is refused and nothing is written.
|
|
101
|
+
Transcript writes and `emet_session_open` fail the same way
|
|
102
|
+
([GUARDRAILS-AND-MONORAILS.md](GUARDRAILS-AND-MONORAILS.md)). The record of which startup
|
|
103
|
+
pages each host fetched is kept in the server's memory for 60 minutes after its last page, so
|
|
104
|
+
after every deploy or restart, or a longer pause, an open with an older `load_id` is refused once
|
|
105
|
+
and told to call `emet_initialize {}` again.
|
|
106
|
+
|
|
107
|
+
There is no setting to turn the monorail's refusals off. In an emergency, redeploy private main
|
|
108
|
+
as it stood before this release (commit-9bf9096) and check EMET_SESSION_GRAPH first: unset or report flags
|
|
109
|
+
skipped steps without blocking them (a closed session still takes no new exchanges, and a
|
|
110
|
+
session-store outage still blocks transcript writes and `emet_session_open`); enforce keeps
|
|
111
|
+
refusing; off stops consulting the graph, including the closed-session lock, so a closed session
|
|
112
|
+
takes new exchanges again, a session-store outage blocks nothing, and closes are not recorded. That also rolls back every other change in this release, including
|
|
113
|
+
byte-paged read_doc.
|
|
114
|
+
|
|
115
|
+
When a close returns `complete`, the server records the session as closed before it sends the
|
|
116
|
+
close's reply, **when that write succeeds**. A failed write is retried once; if it still fails, the
|
|
117
|
+
reply says `closed_recorded: false` with a warning (the session is not locked yet; calling the close
|
|
118
|
+
again records it).
|
|
119
|
+
|
|
120
|
+
**Per-bot close records.** To run a bot in its own session, add `bots/<source tag>/HANDOFF.md` to
|
|
121
|
+
`EMET_RECORD_DOCS` (comma list), e.g. `bots/intake-bot/HANDOFF.md`. That one id makes the tag a
|
|
122
|
+
bot tag: its sessions close against `bots/<tag>/HANDOFF.md` with seven receipts and no board, only its
|
|
123
|
+
own revisions count, and `write_doc`, `patch_doc`, `retire_doc`, `rename_doc` and
|
|
124
|
+
`accept_nonconformance` refuse it the main handoff and board.
|
|
125
|
+
`retire_doc` and `rename_doc` are refused to a bot tag for every document, not only the main
|
|
126
|
+
records (retiring and renaming are the owner's and the hub's), and `emet_setup_complete` is refused
|
|
127
|
+
to a bot tag.
|
|
128
|
+
Do not map a bot's handoff to `templates/HANDOFF.md`: that template asks for a board a bot does
|
|
129
|
+
not keep, so every bot close would be marked nonconforming. Bot handoffs have their own form,
|
|
130
|
+
`templates/BOTHANDOFF.md`; while it is DRAFT, leave bot handoffs unmapped, and once it is IN FORCE
|
|
131
|
+
add `bots/<tag>/HANDOFF.md=templates/BOTHANDOFF.md` to `EMET_TEMPLATE_MAP` (it marks conformance and
|
|
132
|
+
never rejects a write). Setting `EMET_RECORD_DOCS` replaces its value: keep every entry already
|
|
133
|
+
there and append. Nothing else is set, and no data changes shape. To undo it for one bot, remove
|
|
134
|
+
its id from `EMET_RECORD_DOCS`: that tag goes back to the host rules (it cannot close without the
|
|
135
|
+
main records) and the write refusal lifts. The id must match exactly (no leading `./`, the tag's
|
|
136
|
+
exact case); a typo silently leaves the tag on the host rules and lifts the write refusal. Register
|
|
137
|
+
the tag in `EMET_SOURCE_TAGS` as well, or its transcript appends are refused. Never list the hub's
|
|
138
|
+
own tag (e.g. `hub-bot`): a listed tag can no longer write the main records.
|
|
139
|
+
The full procedure — tag naming, the roster (`templates/ROSTER.md`), the session loop and
|
|
140
|
+
corrections — is [BOTS.md](BOTS.md).
|
|
141
|
+
|
|
142
|
+
**Write guards on records (both always on).** `write_doc`, `patch_doc`,
|
|
143
|
+
`retire_doc`, `rename_doc` and `accept_nonconformance` apply two more refusals, and nothing is written
|
|
144
|
+
when either fires:
|
|
145
|
+
- *Registered hosts only on the main records.* When `EMET_SOURCE_TAGS` is set, a write to the main
|
|
146
|
+
handoff or board (`EMET_HANDOFF_DOC`, `EMET_STATE_DOC`) is refused unless its `source` is one of the
|
|
147
|
+
listed tags. A write with no `source` is refused too, and so is a misspelled or unlisted tag
|
|
148
|
+
(scheduled runs such as `cloud-scheduled-*` included). Before turning this on, make sure every tag
|
|
149
|
+
that writes the main records - the hub's own tag in particular - is in `EMET_SOURCE_TAGS`. With
|
|
150
|
+
`EMET_SOURCE_TAGS` unset nothing changes.
|
|
151
|
+
- *A bot writes only its own folder; registered tags only under `bots/`.* A bot tag (one whose
|
|
152
|
+
`bots/<tag>/HANDOFF.md` is in `EMET_RECORD_DOCS`) is refused any write under `bots/` outside
|
|
153
|
+
`bots/<tag>/`: other bots' records and `bots/ROSTER.md` belong to the hub. With `EMET_SOURCE_TAGS`
|
|
154
|
+
set, a write under `bots/` by anything other than a listed bot also needs a registered tag: an
|
|
155
|
+
unregistered or misspelled tag, a write with no `source` (it falls back to `EMET_SOURCE_TAG`), and
|
|
156
|
+
`cloud-scheduled-*` runs are refused. Registered host tags still write anywhere under `bots/`.
|
|
157
|
+
|
|
158
|
+
**Known gap (F-2, later).** A tag that is registered but not listed as a bot is a host for both
|
|
159
|
+
guards, so it can still write the main records and any bot's folder. To fence a bot, set both
|
|
160
|
+
variables in one change. **With `EMET_SOURCE_TAGS` unset, both guards are opt-in and off:** any
|
|
161
|
+
caller can write the main records and every bot folder, so set the registry on any shared install.
|
|
162
|
+
|
|
163
|
+
**Legacy `CASCADE_` names.** Only the settings read through `setting()` in `src/env.js` also accept
|
|
164
|
+
the legacy `CASCADE_...` name as a deprecated fallback (the `EMET_` name wins when both are set):
|
|
165
|
+
`EMET_SOURCE_TAG`, `EMET_SOURCE_TAGS`, `EMET_OAUTH_PASSPHRASE`, `EMET_AUDIT_LOG`, `EMET_HANDOFF_DOC`,
|
|
166
|
+
`EMET_STATE_DOC`, `EMET_INSTALL_DOC`, `EMET_CHARACTER_DOC`, `EMET_TEMPLATE_MAP`, `EMET_RECORD_DOCS`,
|
|
167
|
+
`EMET_READ_DOC_PAGE`, `EMET_SIGNING_KEY`, `EMET_GAP_BASELINE`, `EMET_CIMD`, `EMET_INVITES`,
|
|
168
|
+
`EMET_TIMEZONE`, `EMET_CLOSE_GRACE_MINUTES` and `EMET_ACTIVE_GEMS`. These read `process.env` directly, so **only the
|
|
169
|
+
`EMET_` name works** and a `CASCADE_` name is ignored: `EMET_COLLECTION_PREFIX`,
|
|
170
|
+
`EMET_VECTOR_INDEX`, `EMET_CORPUS_INDEX`, `EMET_CORPORA`, `EMET_CORPUS_COLLECTION`,
|
|
171
|
+
`EMET_REQUEST_LOG` and `EMET_CRASH_LOG`. (`MONGODB_URI`, `MONGODB_DB`, `PUBLIC_URL`, `PORT`, `HOST`,
|
|
172
|
+
`LOG_LEVEL`, `LOG_FORMAT` and `DEBUG` have no prefix. `TRUST_PROXY` is not read: the server trusts exactly one proxy hop (Railway's edge).) Use the `EMET_` names.
|
|
173
|
+
|
|
174
|
+
**The two-pass deploy is not optional.** `PUBLIC_URL` is the OAuth issuer; until it matches
|
|
175
|
+
the real URL, authorization fails in ways that look like a client problem.
|
|
176
|
+
|
|
177
|
+
The repository's `Dockerfile` starts the HTTP transport (`node src/http-remote.js`), and Railway
|
|
178
|
+
builds from it — there is no start command to set. (If you deploy without the Dockerfile, the
|
|
179
|
+
start command is `npm run start:http`.)
|
|
180
|
+
|
|
181
|
+
**Would rather have your assistant run all of this?** [GUIDED-SETUP.md](GUIDED-SETUP.md).
|
|
182
|
+
|
|
183
|
+
### If the CLI misbehaves on Windows
|
|
184
|
+
|
|
185
|
+
Two things cost real time and neither is obvious:
|
|
186
|
+
|
|
187
|
+
- **The npm `.cmd` shim fails with `'"node"' is not recognized`** if node is not on PATH in the
|
|
188
|
+
shell that launches it. The package ships a native binary - call it directly:
|
|
189
|
+
`%APPDATA%\npm\node_modules\@railway\cli\bin\railway.exe`
|
|
190
|
+
- **`railway login --browserless` only works if you approve on the SAME machine.** The flow
|
|
191
|
+
redirects to `127.0.0.1`, so approving the code on your phone or another computer completes
|
|
192
|
+
nothing and the CLI waits forever. Plain `railway login` opens the right browser locally and
|
|
193
|
+
is more reliable.
|
|
194
|
+
|
|
195
|
+
Non-interactive linking needs explicit flags:
|
|
196
|
+
`railway link --project <name> --service <name> --environment production`
|
|
197
|
+
|
|
198
|
+
## Fly.io (alternative)
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
fly launch --no-deploy
|
|
202
|
+
fly secrets set MONGODB_URI=... MONGODB_DB=emet PUBLIC_URL=https://<app>.fly.dev \
|
|
203
|
+
EMET_OAUTH_PASSPHRASE=... EMET_SOURCE_TAGS=mobile,desktop,cli
|
|
204
|
+
fly deploy
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Smoke test before connecting anything
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
curl https://<host>/health
|
|
211
|
+
curl https://<host>/.well-known/oauth-authorization-server
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The first should report ok and a tool count. The second should return OAuth metadata, not a
|
|
215
|
+
404 — if it 404s, `PUBLIC_URL` is wrong.
|
|
216
|
+
|
|
217
|
+
## Connecting clients
|
|
218
|
+
|
|
219
|
+
**Claude (all interfaces, including phone):** Settings → Connectors → Add custom connector →
|
|
220
|
+
URL `https://<host>/mcp` → connect → you are sent to a passphrase page → enter
|
|
221
|
+
`EMET_OAUTH_PASSPHRASE`. Enable it per conversation from the "+" menu.
|
|
222
|
+
|
|
223
|
+
**Any other MCP client:** point it at `https://<host>/mcp` and complete the OAuth flow.
|
|
224
|
+
|
|
225
|
+
Then verify from that device: `emet_status` with `probe: true`, followed by a `recall` of something
|
|
226
|
+
you saved elsewhere. If the remote host sees what the laptop wrote, the deployment is real.
|
|
227
|
+
|
|
228
|
+
## Notes
|
|
229
|
+
|
|
230
|
+
- **Release note: startup stage 1 (the ref 358 amendment).** Three behaviour changes take effect
|
|
231
|
+
live on deploy:
|
|
232
|
+
- **Re-running setup is refused.** `emet_setup_complete` on a store where both
|
|
233
|
+
`bootstrap/IDENTITY.md` and `bootstrap/USER.md` already exist is refused unless the call passes
|
|
234
|
+
`reissue: true` (a top-level boolean) on the owner's explicit word; nothing is written. A host
|
|
235
|
+
flow that re-runs setup at startup now gets a refusal. That is intended: tell the owner.
|
|
236
|
+
- **Bots lose `retire_doc` and `rename_doc` everywhere,** their own `bots/<tag>/` folder
|
|
237
|
+
included. Retiring and renaming are the owner's and the hub's. `emet_setup_complete` is also
|
|
238
|
+
refused to a bot tag.
|
|
239
|
+
- **`emet_initialize` no longer creates the owner person.** Only `emet_setup_complete` does. An
|
|
240
|
+
older install that was set up before the owner row existed, and never had one created, keeps
|
|
241
|
+
resolving its owner as the legacy subject `owner` until the owner runs `emet_setup_complete`
|
|
242
|
+
once with `reissue: true` (which also writes new versions of the identity documents; prior
|
|
243
|
+
versions are archived). There is no separate tool for this.
|
|
244
|
+
- **Clients cache tool lists, and this WILL catch you.** After any deploy that changes the
|
|
245
|
+
tool surface, every host keeps offering the old set until you reconnect the connector there.
|
|
246
|
+
Nothing breaks - existing tools keep working - but new tools stay invisible. Reconnect each
|
|
247
|
+
host: phone, desktop, CLI, anything else. This is the last-mile step people forget.
|
|
248
|
+
- **Costs.** A small Atlas cluster plus a hobby-tier host runs in the low tens of dollars a
|
|
249
|
+
month. Both have free tiers adequate for a single user.
|
|
250
|
+
- **The passphrase is the whole perimeter.** There is no second factor. Rotate it by changing
|
|
251
|
+
the variable and redeploying; existing device authorizations are invalidated.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Operating disciplines — the reasoning behind the nine lines
|
|
2
|
+
|
|
3
|
+
`emet_initialize` serves `operating_discipline`: nine one-line rules (`DISCIPLINE_SUMMARY` in `src/disciplines.js`). This document is the long form — why each rule exists. It was carried inside the code until 2026-09-27, when the code audit found the same rules stated twice in one file with only the short form ever served (finding 3.13). The reasoning is for people; the lines are for the model.
|
|
4
|
+
|
|
5
|
+
Each rule exists because its absence caused a real, traced loss of information in production use.
|
|
6
|
+
|
|
7
|
+
These disciplines are **guardrails**: they bound the field in which the model uses its judgment. The steps that must never depend on judgment (open, append every exchange, board before handoff, close in order) run on **the monorail**, the server-checked session graph. See [Guardrails & Monorails](GUARDRAILS-AND-MONORAILS.md).
|
|
8
|
+
|
|
9
|
+
**Recall before assuming.** Before answering anything that may have prior context — a decision, a preference, a configuration, a name — search memory first. Answering from what seems likely, when the store would have told you, is the most common way a memory system silently stops mattering.
|
|
10
|
+
|
|
11
|
+
**Verify every write.** A save is not done because the call returned. Read it back. A claimed save that was never verified is worse than no save, because it stops anyone from looking again. This applies to reads too: before acting on a recorded path, id, or fact, confirm it still exists.
|
|
12
|
+
|
|
13
|
+
**The two tiers stay independent.** Layer entries hold curated meaning. Transcripts hold the word-for-word exchange. They are deliberately separate: when a layer entry is relevant but thin, the transcript around its timestamp holds the depth that was compressed out. Summarising at transcript-write time collapses the two into one and destroys the second layer of inquiry. Store transcripts verbatim. If a payload will not transmit, split it across parts — never compress it.
|
|
14
|
+
|
|
15
|
+
**Supersede, never mutate.** Correcting the record means writing a new entry that supersedes the old one, not editing or deleting it. The superseded entry stays reachable. It is marked with the date it ended and what replaced it. The current entry is listed first in every order, including a request for the latest and a timestamp sort. It does not fade and it is not hidden. A revision must stand alone and carry the dated specifics it is based on — an abstracted principle is weaker than the sentence someone actually said. Events are immutable: never revise an episodic entry.
|
|
16
|
+
|
|
17
|
+
**Consolidate late.** Wait for roughly three instances of a stable pattern before merging observations into one standing entry. Merging early buries the differences that turn out to matter.
|
|
18
|
+
|
|
19
|
+
**Provenance is part of the memory.** Every write records which host produced it. Set a distinct source tag per host. A shared default tag is not attribution — it is ambiguity that becomes invisible the moment a second host connects, and it makes forks impossible to see.
|
|
20
|
+
|
|
21
|
+
**Findings, not explanations.** When something in the record is unaccounted for, write down what was observed and stop. Do not attach a cause you did not verify. A plausible invented explanation is harder to dislodge later than an open question, and the most dangerous inventions are the reassuring ones.
|
|
22
|
+
|
|
23
|
+
**Mark inference as inference.** State what is known, what is inferred, and what is unknown, as three different things. If a value has not been observed, record it as unknown rather than supplying a likely one.
|
|
24
|
+
|
|
25
|
+
**Write at the moment.** Save when it happens, not at the end. A problem solved, a decision made with its reasoning, a stated preference, a correction — each is worth an entry while the specifics are still exact. Long working sessions deserve progress entries, because context can be lost without warning.
|
|
26
|
+
|
|
27
|
+
**Name the layer.** Every write names its layer. An entry saved without one is not routed by content — both routers were measured against the record and neither is fit to place material in a durable layer by guess — it lands in the working layer, flagged. It does not age out and it is not hidden. The layer charter says what belongs where; choosing is the writer's job, not the door's.
|
|
28
|
+
|
|
29
|
+
**Hand off before the gap.** Losing context mid-task is a failure. Before any interruption, restart, or handoff, write down what was in progress, why it stopped, and the exact next step. The handoff is the thread that survives the gap.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Guardrails & Monorails
|
|
2
|
+
|
|
3
|
+
*An EMET architectural practice: how the memory function keeps an AI's work consistent and its record whole, without leaving it to the model to choose.*
|
|
4
|
+
|
|
5
|
+
## The idea
|
|
6
|
+
|
|
7
|
+
An AI working with persistent memory does two kinds of work:
|
|
8
|
+
|
|
9
|
+
- **Judgment work:** reading a person, weighing a decision, writing a reply, deciding what is worth saving.
|
|
10
|
+
- **Process work:** opening the session, appending each exchange to the transcript, writing the board before the handoff, closing in order.
|
|
11
|
+
|
|
12
|
+
Judgment work needs room to move. Process work needs to happen the same way every time, whether or not the model remembers or feels like it. EMET treats these two differently, and the name for that is **Guardrails & Monorails**.
|
|
13
|
+
|
|
14
|
+
- **Guardrails** bound a field. Inside them the AI moves freely and does its processing. The guardrails keep that processing honest and in character. In EMET, these are the floor, the character and user documents, and the served instructions (`operating_discipline`, `tools_in_force`, `session_close_protocol`).
|
|
15
|
+
- **The monorail** carries the AI from one field to the next without asking it to decide. In EMET, this is the **session graph** (`src/session-graph.js`), the state-driven track a session runs on, checked by the server on every write.
|
|
16
|
+
|
|
17
|
+
In the words of the owner, who named the practice (2026-09-29):
|
|
18
|
+
|
|
19
|
+
> "Guardrails give a field of movement and execution to allow an AI to do processes, where rails guide the AI through a process flow without it processing, to bring it to another space where it expands into a processing space with guardrails."
|
|
20
|
+
|
|
21
|
+
### Why a monorail
|
|
22
|
+
|
|
23
|
+
The practice was first called Guardrails & Rails. It was renamed Guardrails & Monorails the same night, as the more accurate picture of what guidelines and graphs do together.
|
|
24
|
+
|
|
25
|
+
A monorail car straddles its beam: it can't derail, and it can't leave the track.
|
|
26
|
+
|
|
27
|
+
The board is a guardrail with no write door. The server provisions it. After that the only change is an edit with a reason, and a change that leaves any document under half the size it had when this session first touched it is refused. A monorail does have switches, but they belong to the track. A section of the beam moves, and the system throws it, not the car. The car runs from station to station, and its doors open only when it's stopped at a station.
|
|
28
|
+
|
|
29
|
+
- **The stations** are the processing spaces: a workshop, a kitchen, a study. The AI steps out at each one and works freely inside the guardrails.
|
|
30
|
+
- **The beam** is the protocol order: open, work, record, board, handoff, close.
|
|
31
|
+
- **The switches** sit at junctions on the beam. Each one routes the AI to one of several guardrail spaces, where it works in a bounded way. The track throws the switch; the AI rides it.
|
|
32
|
+
- **The doors** are the server's checks. A station's door opens only when every stop before it has been made.
|
|
33
|
+
|
|
34
|
+
The same thing can be pictured as a building, with **hallways and rooms**. The rooms are where work happens, and the hallways carry you to the next room without any decisions along the way.
|
|
35
|
+
|
|
36
|
+
It resembles Chutes & Ladders only on the surface, as a board with fixed paths between squares. In that game the paths are chance. In EMET they are set by the state of the session, and they never move the AI backward as a penalty. A door that stays shut says exactly which stop is owed and how to make it.
|
|
37
|
+
|
|
38
|
+
## Why a monorail, and why in the server
|
|
39
|
+
|
|
40
|
+
Guidelines alone depend on the model choosing to follow them. A model under a long context, a cut-off payload or a lost turn will sometimes not follow them. The record then has a hole that nobody sees. EMET's standing rule is that integrity is a property of the store, not an option a model can decline (see *The door* in the README). The monorail applies that same rule to the session's order of work.
|
|
41
|
+
|
|
42
|
+
The monorail lives in the **EMET MCP server**, not in any host. There's no Claude Code hook, no CLI step and no model-specific tool. Any AI, on any desktop or phone, that can call EMET's tools rides the same track, because the check is made where the write lands.
|
|
43
|
+
|
|
44
|
+
## How the monorail works
|
|
45
|
+
|
|
46
|
+
Every governed write (`save_to_layer`, `revise_memory`, `write_doc`, `patch_doc`, `retire_doc`, `rename_doc`, `accept_nonconformance`, `emet_transcript_append`, `save_transcript`, `revise_transcript`, `emet_session_close`) names its session, either as `session_id` or as the session in `derived_from` (for `emet_session_close`, `receipts.transcript_session_id` when `session_id` is omitted, the same session the close checks), and carries `exchange`. Before the write runs, the server reads the session's state from `<prefix>sessions` (opened, board, episodic, handoff, closed) and the transcript's highest appended exchange. It then decides whether the door opens.
|
|
47
|
+
|
|
48
|
+
**Writes outside the graph, on purpose.** Four tools write without a session and are not governed: `emet_setup_complete` (records the first-time interview as the bootstrap documents and the owner; it runs before the store has an identity, and a host mid-setup must never be refused for lacking a session it may not have opened yet, though it can open one with its setup `load_id`); `emet_invite_create` and `emet_member_access_create` (the owner's access codes, kept in their own collections, not the record); and `emet_status` with `probe: true` (writes one probe document, reads it back and removes it to prove the write path). `emet_session_open` has its own gate (`startup_pages`). Every other write tool is governed.
|
|
49
|
+
|
|
50
|
+
| Gate | The door stays shut when | What the refusal says to do |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `no_session` | a governed write names no session at all | open one with `emet_session_open` (with the `load_id` from your startup pages), or use the `session_id` already opened, then resubmit the same write with it |
|
|
53
|
+
| `not_open` | the write names a session that was never opened | open it with `emet_session_open` (with the `load_id`), then resubmit the same write with the `session_id` it returns |
|
|
54
|
+
| `turn_skipped` | exchange *n* is declared but *n−1* was never appended | append the named exchange with `emet_transcript_append` (this `session_id`, that `exchange_index`), then resubmit the same call |
|
|
55
|
+
| `handoff_early` | `HANDOFF.md` is written before the board, the episodic record and the transcript | write each missing step with its tool (the board with `write_doc` or `patch_doc`, the episodic record with `save_to_layer` layer `episodic`, the transcript with `emet_transcript_append`), then the handoff again |
|
|
56
|
+
| `startup_pages` | `emet_session_open` is called without a `load_id`; with a `load_id` this server has no record of (made up, not served by this process, expired 60 minutes after its last page, or from before a restart), since its pages cannot be checked; or while pages of that load were never fetched (never checked against another host's load) | pass `load_id` from your startup (`startup_page.load_id`, carried by every `emet_initialize` result: each page, `page: "all"`, a one-result startup and a store not set up yet); for a load with no record, call `emet_initialize {}`, fetch every page and open with the `load_id` it returns; for missing pages, the call comes first: the exact `emet_initialize {"page": N, "load_id": ...}` for the first missing page, the missing pages once as ranges (`2-40`), then open again with that `load_id` |
|
|
57
|
+
| `closed` | a write arrives after a complete close - except the one closing exchange inside the grace window, and a transcript correction | nothing in this session: the work belongs in the next session opened the usual way (never one opened just for it) |
|
|
58
|
+
|
|
59
|
+
Every refusal is made before the tool runs, so nothing is written, and its reply carries the pass instruction above (each fits inside the 500-character error-message cap). The same holds for the source-tag refusals on transcript writes, which lead with `pass "source"`: a missing or unregistered tag is told to pass a registered tag, and the refusal does not list the registered tags. Only an exact member of `EMET_SOURCE_TAGS` counts. A `cloud-scheduled-` prefix does not. Any id or tag the caller sent is echoed at most 64 characters, so no refusal is cut before its instruction.
|
|
60
|
+
|
|
61
|
+
Every governed result carries a `session_graph` block: `{mode, verdict, gate, reason, next}`. `next` is the stop after this one, taken from the state, so the AI never has to work out where it is. After a successful call, the server records the stop. A governed write that names a session fails closed when the graph cannot be read: nothing is written, and the reply says the session graph is unavailable. A write that names no session is refused by the `no_session` gate before the graph is read. (The closed-session checks on the transcript doors and `emet_session_open` fail safe instead, below.)
|
|
62
|
+
|
|
63
|
+
**A closed session takes no new exchanges, except its closing exchange** (2026-10-01, the owner's decision). In the closing turn the assistant writes the handoff, gets `complete`, and only then replies, so the append of that last exchange always arrives after the close. A closed session therefore takes exactly **one** more `emet_transcript_append`: numbered highest+1, within `EMET_CLOSE_GRACE_MINUTES` (default 5; `0` turns the window off) of the recorded close time. The reply's `closing_exchange` says it was accepted and that the session is now locked. The closing exchange is written into the session's last live part, whatever session id the append names. A retry of that same exchange number is answered as already saved by the check itself; the append tool is not called, so a retry writes nothing, at any time. Ids are grouped the way the close check groups them (`<base>`, `<base>_verbatim_partN`, and any other `<base>_...` id all belong to `<base>`), and `exchange_index` is checked once (a whole number of 1 or more, or its text) before the monorail, the check and the tool read it. After that one append, or once the window passes, appends are refused with a reply that says the session is closed and locked, nothing was written, and the exchange belongs in the next session opened the usual way. `save_transcript` into a closed session is refused. `revise_transcript` is **allowed** (a correction supersedes and deletes nothing), but on a closed session a revision may only correct the exchanges its part already holds, never add new ones, and must carry exactly the same exchange numbers (none dropped or repeated; numbers sent as text are stored as numbers), so the closed session's exchanges cannot grow, shrink or be renumbered. These checks always run before the tool and fail safe: if the session's state, its transcript position or the part to be revised can't be read, nothing is written and the reply says to send the same call again. They decide every transcript write into a closed session (the graph defers to them), and their refusals are logged in the lab as `refuse`, gate `closed`. `EMET_CLOSE_GRACE_MINUTES` is capped at 60; a negative or non-numeric value (hex such as `0x10` included) is logged and the default 5 is used.
|
|
64
|
+
|
|
65
|
+
**The close is recorded before its reply, when the write succeeds.** When `emet_session_close` returns `complete`, the server writes the closed flag and its time (`closed_at`) to `<prefix>sessions` and only then replies, with `closed_recorded: true`. If that write fails it is retried once; if it fails again the reply says `closed_recorded: false`, with a warning in `closed_record_warning`, `warnings` and `instructions`: the session is not locked until a later close records it.
|
|
66
|
+
|
|
67
|
+
**Bot sessions close against their own records** (per-bot close records, 2026-10-01, the owner's decisions). Each bot runs in its own session, and the main `HANDOFF.md` and `STATE.md` hold only the owner's hub conversation. A tag is a bot tag when `bots/<tag>/HANDOFF.md` is listed in `EMET_RECORD_DOCS`; the session's tag is the source tag on its transcript. For a bot tag's write of its own `bots/<tag>/HANDOFF.md`, the `handoff_early` gate and the "handoff written" step work as they do for the main handoff, without the board condition (a bot keeps no board unless it lists one as a working document), and they refuse like every other gate. The gate keys on the write's `source`; a write with no `source` takes the host path. The bot's close itself checks `bots/<tag>/HANDOFF.md` and seven receipts (no board; `handoff_doc` names the bot's handoff), counts only that tag's own revisions in the window, is `blocked` if a receipt names the main handoff or board, and `incomplete` if the bot's tag revised either. `write_doc`, `patch_doc`, `retire_doc`, `rename_doc` and `accept_nonconformance` refuse the main handoff and board under a bot tag, and nothing is written. The same five tools also refuse a bot tag any write under `bots/` outside its own `bots/<tag>/` (other bots' records and `bots/ROSTER.md`), and when `EMET_SOURCE_TAGS` is set they refuse the main handoff and board to any `source` not listed in it, and refuse any write under `bots/` by anything other than a listed bot unless its tag is listed, a missing `source` and `cloud-scheduled-*` included. A registered tag that is not listed as a bot can still write the main records and any bot's folder (a known gap, F-2). Since startup stage 1, `retire_doc` and `rename_doc` are refused to a bot tag for every document, its own `bots/<tag>/` folder included (retiring and renaming are the owner's and the hub's), and `emet_setup_complete` is refused to a bot tag. Before either guard, the same five tools refuse a look-alike doc id: a leading `/`, a `.` or `..` segment, or `bots/` in the wrong case (`Bots/`); also a control or invisible formatting character anywhere (zero-width space, BOM, direction overrides), leading or trailing whitespace on the id or any segment, a backslash, the same `/`, `.`/`..` and `bots/` shapes after Unicode NFKC folding (full-width dots and slashes, `‥`, full-width letters; the first segment must be the ASCII `bots`), and a case or Unicode variant of a main-record id (`handoff.md`, ` HANDOFF.md`). Doc ids are flat strings, never resolved as paths or URL-decoded; there is no confusables table. One exception, for cleaning up ids written before these checks: `retire_doc`, and `rename_doc` from such an id onto a clean one, are let through for a tag listed in `EMET_SOURCE_TAGS` that is not a bot tag (never for a bot, an unlisted or missing tag, or with no registry set). With no registry set, both guards are opt-in and nothing changes. A session has one tag: if its transcript parts carry more than one and any is a bot tag, the close is `incomplete`. Host closes keep their eight receipts and their verdict; revisions by other tags that the full rule would not count are listed in `attribution_warnings`.
|
|
68
|
+
|
|
69
|
+
### Mode: enforce only
|
|
70
|
+
|
|
71
|
+
Since 2026-10-03 the monorail runs in enforce only. There is no report mode and no off switch; `EMET_SESSION_GRAPH` is no longer read. Every skipped step is refused before the tool runs, the reply says how to pass, and the refusal is logged in the lab. Lab rows with verdict `would_refuse` are from report mode, before that date.
|
|
72
|
+
|
|
73
|
+
### Switches: small logic at each junction
|
|
74
|
+
|
|
75
|
+
*Design stated by the owner, 2026-09-29. Not built yet: the live monorail is one straight line with the four gates above.*
|
|
76
|
+
|
|
77
|
+
The architecture needs flexibility of flow: at some points a session should go one way or another, into a different guardrail space, depending on what is happening. That choice must not cost heavy processing, and it must not be left to the model. So each junction gets a **switch**: a small, purpose-built logic unit inside EMET.
|
|
78
|
+
|
|
79
|
+
- **Like a transistor (an IGBT).** A switch conducts only when its gate condition is met, and it is tripped by that condition, nothing else. The condition is read from what EMET already holds: the session's state, the call being made, and what the record says. The branch it selects is where the session goes next.
|
|
80
|
+
- **Small and local.** A switch is a few lines of logic in the server's code. No hosted model, no outside API, no model call. It gives the same answer for the same inputs, every time, so it can be tested like any other gate. This is the role a fast external decision model (such as JEV) plays in other designs, done here by the track itself.
|
|
81
|
+
- **Designed per junction.** Each switch's design follows the function it serves and how information and tasking flow through that junction. There is no general-purpose router: a junction gets a switch only when a real fork exists, and the switch knows only what that fork needs (commitment 6: over-engineering is a defect).
|
|
82
|
+
- **Bounded on every branch.** Each branch leads to a guardrail space, for example a GEM (Golem Expertise Module) once the socket exists, or a work instruction for a kind of task. The four gates still apply on every branch, so a switch can change where the session works, never whether its steps are recorded.
|
|
83
|
+
- **Recorded.** Each time a switch trips, the lab results record which way it went and why, so its conditions can be checked against real sessions before anything depends on them.
|
|
84
|
+
|
|
85
|
+
### Lab results
|
|
86
|
+
|
|
87
|
+
Every check the monorail makes (ok, refuse, unknown; `would_refuse` and `late` in rows from report mode) is written the moment it happens as one timestamped row in `<prefix>graph_lab`. Each row records the time, session, host (source tag), tool, the exchange the call declared, the transcript's highest appended exchange at that moment, the mode, the verdict, the gate and the reason. Rows are only ever added, never changed or removed. A failed lab write is logged and never slows or blocks the call.
|
|
88
|
+
|
|
89
|
+
`emet_gaps` serves the summary as `graph_lab`: the total number of checks, first and last timestamps, the number of sessions, counts by verdict, gate and host, and the flagged rows (anything not ok) newest first. Because it's a tool call, any AI on any device can pull the lab results.
|
|
90
|
+
|
|
91
|
+
**Startup pages fetched late** (2026-10-03). In enforce only an open with a page missing is refused, so no session opens without its pages and no `late` row is written through the server's door. The summary still pairs `late` rows with a `would_refuse` open (a page fetched late is not still missing, and an open whose every missing page came late is not flagged), for any such rows written in report mode; an open refused stays flagged.
|
|
92
|
+
|
|
93
|
+
**One process only** (2026-10-03). The record of which startup pages were fetched, which the `startup_pages` gate reads and the late pairing uses, is held in the memory of the one server process that served them. Pages served by another instance are not on it, so this gate assumes EMET runs as a single process; with several instances it could refuse an open whose pages were fetched from another instance. A restart, or 60 minutes without a page of that load being served (its own lifetime; the cache of startup payloads still keeps them 10 minutes), clears it, and a load with no record is refused with the way back (a fresh `emet_initialize {}`), so after a restart or a long pause before the open every host fetches its startup again, once. Pages are never taken as checked when they were not.
|
|
94
|
+
|
|
95
|
+
**How to read them:** each flagged row is either a real skipped step that was refused (the monorail working) or a gate that is wrong (fix the gate).
|
|
96
|
+
|
|
97
|
+
## The honest limit
|
|
98
|
+
|
|
99
|
+
The server sees tool calls, never the reply text. It can confirm that exchange 8 was appended before exchange 9 was declared. It can't confirm that exchange 8 holds what was actually said, or that a reply was given without a call at all. Turn coverage is only as strong as the exchange numbers hosts report. The guardrails (character, floor, disciplines) carry what the monorail can't reach, and `emet_gaps` counts whatever holes remain.
|
|
100
|
+
|
|
101
|
+
## Where it lives
|
|
102
|
+
|
|
103
|
+
- `src/session-graph.js`: the gates (`evaluate`), the next stop (`nextStep`), the recorded stops (`recordStep`) and the thin store layer (`graphBefore`, `graphAfter`).
|
|
104
|
+
- `src/dispatch.js`: the checkpoint before every governed tool and the `session_graph` block on every result.
|
|
105
|
+
- `src/tools.js`: `session_id` and `exchange` added to every governed schema from the graph's own tool list.
|
|
106
|
+
- `src/setup.js`: `tools_in_force` and the short instructions tell every host to pass both fields and follow `session_graph.next`.
|
|
107
|
+
- `tests/session-graph.test.js`: the gates, the next stop, and the fail-closed door when a named session cannot be read.
|
|
108
|
+
- Design record: `staging/emet-session-graph.md` in the EMET store. The framing is in semantic memory entry 297, and the decision on switches in procedural ref 402.
|