@reunionstudio/airlock-mcp 0.1.8 → 0.1.10

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.
@@ -83,6 +83,12 @@ deterministic checker for local spec drafts.
83
83
  `airlock.admin.sync_okf_bundle_metadata(...)`, and exposes authoritative
84
84
  accepted context through `AIRLOCK_DATA.ACTIVE.V_OKF_CONCEPT_METADATA`.
85
85
  Draft and rejected bundles are not authoritative agent context.
86
+ Use OKF v0.2 `generated`, `sources`, `verified`, and lifecycle metadata;
87
+ legacy v0.1 bundles still work. See the pattern README for field shapes.
88
+ Verification and `status: stable` are producer claims, not Airlock approval
89
+ or authenticated identities. Neither `human:` nor other producer labels
90
+ grant authority. Never execute bundle computations or attesters as part of
91
+ validation/loading. Airlock retains its bounded Markdown-only profile.
86
92
  6. Use `airlock-mcp import-spec <json-file> <name>` when starting from a
87
93
  spec-library file, an exported `SPEC_CONFIG`, or an existing canonical config.
88
94
  If the source is from `airlock-specs`, treat it as a reusable draft pattern.
@@ -229,6 +235,17 @@ Airlock:
229
235
  - `airlock.admin.*` performs administrative changes and operational mutations.
230
236
  - `airlock.agent.*` performs governed user or agent work in the actor's scope.
231
237
 
238
+ Installed Airlock may also expose separate opt-in Snowflake-managed MCP
239
+ endpoints for `app_agent` and `app_observer`. They are distinct from this local
240
+ spec-workbench MCP server. Administrators manage them with
241
+ `admin.enable_mcp_server`, `admin.disable_mcp_server`, and
242
+ `observe.mcp_servers`. Their fixed allowlists contain five agent tools
243
+ (`airlock_my_access_context`, work, safe spec description, spec states, and file
244
+ state) and four observer tools (specs, work, spec states, and file state). They
245
+ never expose mutation, governed payload rows, restricted-reference reads, or
246
+ attachment bytes. Each client must authenticate as its own Snowflake principal;
247
+ workspace identity is not Airlock authority.
248
+
232
249
  Treat every mutating human or automated agent as an attributable Snowflake
233
250
  principal. Direct work uses that principal's Airlock assignments. Work for
234
251
  another principal requires explicit Airlock delegation and
@@ -10,5 +10,11 @@ work, `airlock.observe.*` for read-only observation, and `airlock.admin.*` only
10
10
  for intentional administration. Use explicit Airlock delegation when the
11
11
  connected principal acts for another principal.
12
12
 
13
+ Do not confuse this local spec-workbench MCP server with the optional
14
+ Snowflake-managed MCP endpoints exposed by an installed Airlock Native App.
15
+ Those role-separated agent and observer endpoints preserve Snowflake identity
16
+ and expose only fixed read-only tools; they do not expose mutation or governed
17
+ payload rows.
18
+
13
19
  Never store credentials in this plugin or its workspace files. Configure the
14
20
  Airlock MCP connection through the supported environment or connection setup.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "airlock-mcp",
3
3
  "description": "Governed Airlock spec, observation, and commitment guidance for Snowflake principals.",
4
- "version": "0.1.8",
4
+ "version": "0.1.10",
5
5
  "author": {
6
6
  "name": "Reunion Studio",
7
7
  "url": "https://github.com/reunionstudio/airlock-mcp"
@@ -15,7 +15,7 @@
15
15
  "command": "npx",
16
16
  "args": [
17
17
  "-y",
18
- "@reunionstudio/airlock-mcp@0.1.8",
18
+ "@reunionstudio/airlock-mcp@0.1.10",
19
19
  "server"
20
20
  ]
21
21
  }
package/README.md CHANGED
@@ -54,6 +54,13 @@ Current Airlock separates procedure intent:
54
54
  specs, validating/loading data, workflow actions, attachments, delegations,
55
55
  and references.
56
56
 
57
+ Installed Airlock can also expose a deliberately small read-only subset through
58
+ separate Snowflake-managed Native App MCP servers for `app_agent` and
59
+ `app_observer`. They are opt-in, preserve the authenticated Snowflake principal,
60
+ and do not replace the public procedure API or this package's local workbench.
61
+ See [Native App managed MCP](docs/native-app-managed-mcp.md) for the fixed tool
62
+ allowlists, setup calls, identity proof, and security boundary.
63
+
57
64
  Every mutating human or automated agent should connect as its own attributable
58
65
  Snowflake principal. When it acts for another principal, use explicit Airlock
59
66
  delegation. Snowflake user type, chat identity, channel membership, workspace
@@ -78,18 +85,21 @@ current work, and
78
85
  `airlock.observe.activity(...)` for historical events. The older split work
79
86
  procedures are retired.
80
87
 
81
- Polling agents should call `airlock.agent.spec_state(...)` before retrieving a
82
- complete descriptor, file list, or governed dataset. Cache the
83
- authorization-scoped `STATE_TOKEN` and perform the heavier read only when it
84
- changes. Poll `airlock.agent.list_my_work(...)` independently because deadlines
85
- and expectation windows can change with time without rotating a spec token.
88
+ Polling agents should call `airlock.agent.spec_state(...)` for one known spec,
89
+ or `airlock.agent.spec_states(...)` for up to 100 known specs under one role
90
+ lens, before retrieving complete descriptors, file lists, or governed datasets.
91
+ Cache each authorization-scoped `STATE_TOKEN` and perform the heavier read only
92
+ when it changes. Poll `airlock.agent.list_my_work(...)` independently because
93
+ deadlines and expectation windows can change with time without rotating a spec
94
+ token.
86
95
  After selecting a logical file, compare
87
96
  `airlock.agent.file_state(...).FILE_STATE_ID`; that UUID rotates when its data,
88
97
  workflow, attachments, exact source references, or retained version history
89
98
  change. Observer clients use
90
- the authorization-scoped, read-only `observe.spec_state(...)` and
91
- `observe.file_state(...)` equivalents. A missing object or revoked observer
92
- grant returns no row, which is the terminal polling signal; authorized query
99
+ the authorization-scoped, read-only `observe.spec_state(...)`,
100
+ `observe.spec_states(...)`, and `observe.file_state(...)` equivalents. Missing
101
+ or inaccessible specs are omitted from a batched result without disclosing the
102
+ cause; that omitted row is the terminal polling signal. Authorized query
93
103
  failures remain procedure errors.
94
104
 
95
105
  ### Observer Access And Seats
package/SECURITY.md CHANGED
@@ -12,8 +12,31 @@ operation surface.
12
12
  - Validates MCP server names before passing them to `codex mcp add`.
13
13
  - Passes npm or GitHub package specs as argv entries, never through a shell.
14
14
  - Rejects package specs with whitespace or control characters.
15
- - Exposes guidance only; it does not read local secrets, call Snowflake, mutate
16
- files, or contact network services.
15
+ - Runs the bundled Python workbench in isolated mode from a trusted launcher;
16
+ project modules, user site-packages, and ambient `PYTHONPATH` do not enter its
17
+ import path. The selected Python interpreter and installed system packages
18
+ remain trusted. `cwd` controls workspace paths, not code discovery.
19
+ - Rejects malformed JSON-RPC envelopes without terminating the server.
20
+ Notifications never dispatch side-effecting workbench tools.
21
+ - Does not read local secrets, call Snowflake, or contact network services.
22
+ Workbench tools write only the project files explicitly requested by the
23
+ caller.
24
+
25
+ ## Installed Native App MCP
26
+
27
+ Installed Airlock can separately expose opt-in Snowflake-managed MCP endpoints.
28
+ They are not hosted by this npm package and do not inherit local workspace
29
+ identity. Their fixed agent and observer allowlists expose only read-only access
30
+ context, work, safe spec discovery, and compact spec/file state. They exclude
31
+ administrative or governed mutation, governed payload rows, restricted-reference
32
+ reads, and attachment bytes.
33
+
34
+ Each client must authenticate as its own Snowflake principal with a role that
35
+ holds the matching Airlock application role and can use the configured
36
+ warehouse. The endpoint adapters call Airlock's existing handlers, so procedure
37
+ authorization, role lenses, observer scope, and license policy remain the
38
+ security boundary. Only fixed adapters in the role-accessible versioned `mcp`
39
+ schema are granted; Airlock's private `internal` schema remains ungranted.
17
40
 
18
41
  ## Production Rules
19
42
 
@@ -36,10 +36,23 @@ product surface.
36
36
 
37
37
  ## MCP Surface
38
38
 
39
+ There are two complementary MCP surfaces:
40
+
41
+ - This package is a local bootstrap and spec-workbench server. It does not call
42
+ Snowflake or become an Airlock authorization layer.
43
+ - Installed Airlock may expose optional Snowflake-managed Native App MCP
44
+ servers over nine fixed read-only adapters. Those endpoints execute inside
45
+ Snowflake as the authenticated principal and retain the existing `agent.*`
46
+ and `observe.*` policy checks.
47
+
48
+ See [Native App managed MCP](native-app-managed-mcp.md) for the installed
49
+ surface. The remainder of this section describes this package's local server.
50
+
39
51
  The bootstrap server exposes:
40
52
 
41
53
  - prompt: `airlock-start`
42
54
  - resource: `airlock://getting-started`
55
+ - resource: `airlock://native-app-managed-mcp`
43
56
  - orientation tool: `airlock_start`
44
57
  - workbench tools: `airlock_doctor`, `airlock_init_repo`,
45
58
  `airlock_init_app_context`, `airlock_list_patterns`,
@@ -0,0 +1,67 @@
1
+ # Native App Managed MCP
2
+
3
+ Installed Airlock can optionally expose separate Snowflake-managed MCP servers
4
+ for agent-scoped and observer-scoped reads. The stored procedures remain the
5
+ authoritative API; MCP is a small transport adapter over existing policy checks.
6
+
7
+ This is distinct from the local `airlock-mcp` package. The package helps agents
8
+ design specs and build Airlock-aware software. It does not automatically query
9
+ Snowflake. The Native App endpoints run in the consumer account and require an
10
+ independently authenticated Snowflake principal.
11
+
12
+ ## Enablement
13
+
14
+ Airlock does not create MCP objects during installation. An app administrator
15
+ must opt in and name a usable warehouse:
16
+
17
+ ```sql
18
+ CALL airlock.admin.enable_mcp_server('agent', 'AGENT_WH');
19
+ CALL airlock.admin.enable_mcp_server('observer', 'OBSERVER_WH');
20
+ CALL airlock.observe.mcp_servers();
21
+ ```
22
+
23
+ Use `validate_only => TRUE` to check the fixed plan without DDL. Disable an
24
+ endpoint with `airlock.admin.disable_mcp_server('agent')` or `'observer'`.
25
+ Disable and re-enable to change its warehouse or refresh its specification
26
+ after an Airlock upgrade.
27
+
28
+ The authenticated Snowflake role needs warehouse access and the matching
29
+ Airlock application role. Each mutating human or automated agent must retain
30
+ its own Snowflake principal; never use a shared MCP credential as a
31
+ meta-permission layer.
32
+
33
+ ## Fixed Read-Only Tools
34
+
35
+ The agent endpoint exposes:
36
+
37
+ - `airlock_my_access_context`
38
+ - `airlock_list_my_work`
39
+ - `airlock_describe_spec`
40
+ - `airlock_spec_states`
41
+ - `airlock_file_state`
42
+
43
+ The observer endpoint exposes:
44
+
45
+ - `airlock_observe_specs`
46
+ - `airlock_observe_work`
47
+ - `airlock_observe_spec_states`
48
+ - `airlock_observe_file_state`
49
+
50
+ Neither endpoint exposes mutation, `admin.*`, governed row reads,
51
+ restricted-reference reads, or attachment bytes. Agent calls retain ordinary
52
+ assignment, role-lens, and named-license checks. Observer calls retain
53
+ assignment-scoped observer access and do not claim a named license.
54
+
55
+ Snowflake's managed SQL transport requires scalar `VARIANT` results, so the
56
+ servers target fixed adapters in Airlock's versioned `mcp` schema. Those
57
+ adapters invoke the same handlers as the public read procedures. Only the fixed
58
+ adapters are granted; Airlock's private `internal` schema remains ungranted.
59
+
60
+ ## Principal Proof
61
+
62
+ Before relying on a new PAT-based client, run Airlock's
63
+ `scripts/run_batch62_mcp_identity_probe.py` with two independently issued,
64
+ role-restricted Snowflake programmatic access tokens. The proof must return two
65
+ distinct matching `USER_ID` values from `airlock_my_access_context`. External
66
+ workspace, channel, bot, or public-key identifiers are provenance only and
67
+ never confer Airlock authority.
@@ -26,12 +26,12 @@ or chat approvals as Snowflake authority.
26
26
 
27
27
  Use state tokens before larger reads:
28
28
 
29
- - `agent.spec_state(...).STATE_TOKEN` for the actor's visible contract, data,
30
- and access state;
29
+ - `agent.spec_state(...).STATE_TOKEN` for one actor-visible spec, or
30
+ `agent.spec_states(...)` for up to 100 known specs under one role lens;
31
31
  - `agent.file_state(...).FILE_STATE_ID` for one visible file's data, workflow,
32
32
  attachment, reference, and retained-version state;
33
- - `observe.spec_state` and `observe.file_state` for a scoped observer principal;
34
- and
33
+ - `observe.spec_state`, `observe.spec_states`, and `observe.file_state` for a
34
+ scoped observer principal; and
35
35
  - `agent.list_my_work` or `observe.work` on a separate cadence for deadlines
36
36
  and current work.
37
37
 
@@ -39,6 +39,11 @@ A missing state row is the terminal result for that cursor. Return to discovery
39
39
  instead of inferring whether the object was deleted, retired, or became
40
40
  inaccessible.
41
41
 
42
+ Plural spec polling preserves first-request order after case-insensitive
43
+ deduplication and omits missing or inaccessible targets without identifying the
44
+ cause. Use it for one logical multi-spec polling cycle; keep the singular call
45
+ for one target.
46
+
42
47
  ## Commitment receipts
43
48
 
44
49
  After every supported `agent.*` or intentional `admin.*` mutation, store a
package/docs/workflows.md CHANGED
@@ -209,17 +209,18 @@ confirm the principal no longer needs `agent.*` or Streamlit before calling
209
209
  `admin.unassign_license(...)`. Never share one Snowflake username across
210
210
  channels merely to simplify setup; that collapses attribution and revocation.
211
211
 
212
- Watcher loops should call `agent.spec_state` before larger reads. Cache its
213
- authorization-scoped `STATE_TOKEN` and retrieve descriptors, file lists, or
214
- governed data only when it changes. Poll `agent.list_my_work` independently
215
- because deadlines and expectation windows can change without rotating the spec
216
- token. Once the app tracks one logical file,
212
+ Watcher loops should call `agent.spec_state` for one known spec or
213
+ `agent.spec_states` for up to 100 known specs under one role lens before larger
214
+ reads. Cache each authorization-scoped `STATE_TOKEN` and retrieve descriptors,
215
+ file lists, or governed data only when it changes. Poll `agent.list_my_work`
216
+ independently because deadlines and expectation windows can change without
217
+ rotating a spec token. Once the app tracks one logical file,
217
218
  compare `agent.file_state(...).FILE_STATE_ID` to detect data replacement,
218
219
  workflow movement, attachment changes, exact-reference changes, or retained
219
220
  version-history changes. Observer principals use the authorization-scoped,
220
- read-only `observe.spec_state` and `observe.file_state` equivalents. A missing
221
- object or revoked grant returns no row as the terminal polling signal;
222
- authorized query failures remain procedure errors.
221
+ read-only `observe.spec_state`, `observe.spec_states`, and `observe.file_state`
222
+ equivalents. Missing or inaccessible specs are omitted from a plural result as
223
+ the terminal polling signal; authorized query failures remain procedure errors.
223
224
 
224
225
  Every mutating human or automated agent should connect as an attributable
225
226
  Snowflake principal. When it acts for another principal, use explicit Airlock
@@ -424,6 +425,11 @@ Do not treat draft or rejected Markdown bundles as authoritative agent context.
424
425
  The MCP local checker validates the spec workspace; bundle validation belongs
425
426
  to Airlock's OKF validator and installed procedures.
426
427
 
428
+ The [pattern README](../patterns/okf-knowledge-bundle/README.md) describes the
429
+ OKF v0.2 profile and backward compatibility. Preserve provenance, verification,
430
+ and lifecycle claims as metadata. Never interpret them as Airlock permission or
431
+ approval, and never run a computation or attester during bundle loading.
432
+
427
433
  ## Start From A Known Process
428
434
 
429
435
  Use this when the process is known but the data shape is not.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reunionstudio/airlock-mcp",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Single-install MCP interface for Airlock agents.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "airlock-mcp": "bin/airlock-mcp.mjs"
23
23
  },
24
24
  "scripts": {
25
- "test": "node test/smoke.mjs"
25
+ "test": "node test/smoke.mjs && node test/mcp-boundaries.mjs"
26
26
  },
27
27
  "files": [
28
28
  "bin",
@@ -4,7 +4,7 @@ Use `okf-knowledge-bundle` when the team wants governed Markdown knowledge for
4
4
  people and agents to use as accepted business context.
5
5
 
6
6
  This pattern is for Airlock's `okf_knowledge_bundle` payload adapter. It is
7
- inspired by Google's Open Knowledge Format, but Airlock applies a narrower
7
+ aligned with Google's Open Knowledge Format v0.2, but Airlock applies a narrower
8
8
  profile so accepted bundles can become governed context instead of loose wiki
9
9
  content.
10
10
 
@@ -25,9 +25,44 @@ content.
25
25
  - Every non-reserved Markdown file is a concept document with YAML frontmatter.
26
26
  - Concept documents require `type`.
27
27
  - Recommended concept fields are `title`, `description`, `resource`, `tags`,
28
- `timestamp`, and `source_links`.
28
+ `generated`, and `sources`. Legacy v0.1 `timestamp` and `source_links` work too.
29
+ - New root indexes should declare `okf_version: "0.2"`.
29
30
  - `index.md` and `log.md` are reserved at every hierarchy level.
30
31
 
32
+ ## V0.2 Example
33
+
34
+ ```markdown
35
+ ---
36
+ type: Metric
37
+ title: Net Revenue
38
+ generated: {by: process:finance-context, at: "2026-09-01T00:00:00Z"}
39
+ verified: {by: process:finance-check, at: "2026-09-02T00:00:00Z"}
40
+ status: stable
41
+ stale_after: "2026-10-01T00:00:00Z"
42
+ sources:
43
+ - id: revenue-policy
44
+ resource: https://example.com/revenue-policy
45
+ ---
46
+ Revenue less refunds and adjustments.[^revenue-policy]
47
+
48
+ [^revenue-policy]: Finance revenue policy
49
+ ```
50
+
51
+ Airlock normalizes a single `verified` object to an array and preserves source
52
+ IDs, producer identities, freshness, and extension fields in each parsed row's
53
+ `metadata`. All v0.2 datetimes need an explicit UTC offset. Optional families
54
+ may be omitted. External body links can be declared by `sources[].resource`
55
+ or legacy `source_links`. The existing timestamp projection uses `generated.at`
56
+ when `generated` exists, otherwise legacy `timestamp`.
57
+
58
+ `status: stable` and `verified` are producer claims, not Airlock approval or
59
+ authenticated identities. A `human:` verifier has no extra authority. Airlock
60
+ workflow, PDP, and separately configured Expectations remain authoritative;
61
+ `stale_after` does not create an Expectation. `Attested Computation` descriptors
62
+ can be stored, but Airlock does not execute computations or attesters, fetch
63
+ their resources, or verify their results. Bundles remain Markdown-only with
64
+ bounded size and strict paths/body links, not universal OKF v0.2 packages.
65
+
31
66
  ## Airlock Contract
32
67
 
33
68
  The spec must declare:
@@ -54,7 +89,11 @@ authoritative only from:
54
89
  AIRLOCK_DATA.ACTIVE.V_OKF_CONCEPT_METADATA
55
90
  ```
56
91
 
57
- Draft and rejected bundles are not authoritative agent context.
92
+ Draft and rejected bundles are not authoritative agent context. In the view,
93
+ normalized frontmatter is under `METADATA:metadata`, not top-level columns
94
+ named after every new OKF field. Verify that the installed Airlock release
95
+ includes the v0.2 metadata update before relying on its normalization; updating
96
+ this local MCP package does not deploy the Native App.
58
97
 
59
98
  ## Local Workbench Notes
60
99
 
@@ -5,12 +5,12 @@
5
5
  {
6
6
  "concept_path": "runbooks/monthly-close",
7
7
  "concept_type": "Runbook",
8
- "bundle_version": "2026.06"
8
+ "bundle_version": "2026.09"
9
9
  },
10
10
  {
11
11
  "concept_path": "metrics/net-revenue",
12
12
  "concept_type": "Metric",
13
- "bundle_version": "2026.06"
13
+ "bundle_version": "2026.09"
14
14
  }
15
15
  ]
16
16
  }
@@ -55,7 +55,7 @@
55
55
  "sample_data": {
56
56
  "sample_data_acknowledged": true,
57
57
  "sample_rows": [],
58
- "edited_sample_data": "concept_path,concept_type,bundle_version\nrunbooks/monthly-close,Runbook,2026.06\nmetrics/net-revenue,Metric,2026.06\n"
58
+ "edited_sample_data": "concept_path,concept_type,bundle_version\nrunbooks/monthly-close,Runbook,2026.09\nmetrics/net-revenue,Metric,2026.09\n"
59
59
  },
60
60
  "attachment_policy": {
61
61
  "attachments_enabled": true,
package/setup.py CHANGED
@@ -27,7 +27,7 @@ def data_files():
27
27
 
28
28
  setup(
29
29
  name="airlock-mcp",
30
- version="0.1.8",
30
+ version="0.1.10",
31
31
  description="Codex-first workbench and CLI for drafting Airlock specs.",
32
32
  packages=find_packages("src"),
33
33
  package_dir={"": "src"},
@@ -1,3 +1,3 @@
1
1
  """Airlock MCP CLI package."""
2
2
 
3
- __version__ = "0.1.8"
3
+ __version__ = "0.1.10"
@@ -20,6 +20,14 @@ The canonical specs live in the specs repo or installed Airlock. Files here are
20
20
  snapshots, samples, generated helpers, and planning context for app development.
21
21
  Refresh them when the canonical spec changes.
22
22
 
23
+ For governed knowledge, the `okf-knowledge-bundle` pattern uses Airlock's bounded
24
+ Markdown-only OKF v0.2 profile with legacy v0.1 support. Producer `verified`,
25
+ `generated`, lifecycle, and computation metadata do not grant Airlock authority
26
+ or approval. Loading never executes computations or attesters. Consume current
27
+ accepted context through `AIRLOCK_DATA.ACTIVE.V_OKF_CONCEPT_METADATA`; normalized
28
+ frontmatter is under `METADATA:metadata`. Verify the installed Airlock release
29
+ supports v0.2 normalization before relying on it.
30
+
23
31
  Airlock's built-in Streamlit Native App is a generic operating and fallback
24
32
  surface. A purpose-built app is the preferred place for domain-specific
25
33
  summaries, calculations, evidence layout, terminology, and controls when those
@@ -42,6 +50,14 @@ Installed Airlock separates procedure intent:
42
50
  - `agent.*`: governed agent work in the actor's scope.
43
51
  - `admin.*`: administrative mutation and operational changes.
44
52
 
53
+ Installed Airlock may separately expose opt-in Snowflake-managed MCP endpoints
54
+ for `app_agent` and `app_observer`. They use fixed read-only tool allowlists,
55
+ preserve each client's authenticated Snowflake principal, and never expose
56
+ mutation, governed payload rows, restricted-reference reads, or attachment
57
+ bytes. They are distinct from the local Airlock MCP spec workbench. Manage them
58
+ with `admin.enable_mcp_server`, `admin.disable_mcp_server`, and
59
+ `observe.mcp_servers`.
60
+
45
61
  Use `agent.list_my_work` for the actor's unified current-work inbox,
46
62
  `observe.work` for account-wide current work, and `observe.activity` for
47
63
  historical events. The older split workflow/expectation work calls are retired.
@@ -139,6 +155,12 @@ Installed Airlock procedure grammar:
139
155
  - `admin.*` is administrative mutation. Do not use retired admin read wrappers;
140
156
  use observe list/detail procedures instead.
141
157
 
158
+ An installed Airlock app may also provide separate opt-in Snowflake-managed MCP
159
+ endpoints for `app_agent` and `app_observer`. Their fixed tools are read-only,
160
+ retain the authenticated Snowflake principal, and exclude mutation and governed
161
+ payload reads. Do not confuse those endpoints with the local Airlock MCP spec
162
+ workbench or treat workspace identity as Airlock authority.
163
+
142
164
  Use `agent.list_my_work` for the current actor's unified work inbox. Use
143
165
  `observe.work` for account-wide current work and `observe.activity` for event
144
166
  history.
@@ -224,6 +246,7 @@ def _manifest(mode: str, entries: list[dict[str, Any]]) -> dict[str, Any]:
224
246
  "required_source_references": "load Draft, list eligible sources, pin exact manifest rows, then advance; SOURCE_REFERENCE_REQUIRED blocks missing evidence",
225
247
  "spec_migration": "immutable two-version lifecycle; list with observe.spec_migrations, run bounded admin migration batches, inspect observe.spec_migration evidence, cancel only before activation, and retire only after the source is drained",
226
248
  "admin": "administrative mutation and operational changes",
249
+ "native_mcp": "optional role-separated Snowflake-managed read-only endpoints; fixed agent and observer tools preserve Snowflake principal identity and never expose mutation or governed payload rows",
227
250
  "restricted_reference": "one-record reference lookup through agent.get_reference_record; do not enumerate protected reference paths",
228
251
  "attachment_preview": "governed Streamlit preview emits metadata-only ATTACHMENT_PREVIEW; MCP clients do not get direct stage access",
229
252
  "application_surface": "built-in Streamlit is a generic operating/fallback surface; purpose-built apps own domain UI and use Airlock as the governed backend",
@@ -86,6 +86,13 @@ plan for more. Do not create the first workspace until the user chooses a path.
86
86
  Create `posts` only when the user wants a shared feedback loop or explicitly
87
87
  asks for the posts pattern.
88
88
 
89
+ For governed Markdown knowledge, use `okf-knowledge-bundle`. The Airlock profile
90
+ supports OKF v0.2 provenance, verification, and lifecycle metadata with legacy
91
+ v0.1 support. Read the pattern README for examples. `verified` and
92
+ `status: stable` are producer claims, never Airlock permissions or acceptance.
93
+ Bundle loading does not execute computations or attesters. Use the bounded
94
+ Markdown-only profile and verify the installed Airlock release has the update.
95
+
89
96
  ## Building Apps With Existing Specs
90
97
 
91
98
  Use this path when the user wants to build an app, dashboard, approval queue,
@@ -126,6 +133,15 @@ Installed Airlock separates procedure intent:
126
133
  - `agent.*`: governed agent work in the actor's scope.
127
134
  - `admin.*`: administrative mutation and operational changes.
128
135
 
136
+ Installed Airlock may also expose separate opt-in Snowflake-managed MCP
137
+ endpoints for `app_agent` and `app_observer`. They are distinct from this local
138
+ spec-workbench MCP server and contain only fixed read-only tools for access
139
+ context, work, safe spec discovery, and compact spec/file state. They preserve
140
+ the authenticated Snowflake principal and never expose mutation, governed
141
+ payload rows, restricted-reference reads, or attachment bytes. Administrators
142
+ manage them through `admin.enable_mcp_server`, `admin.disable_mcp_server`, and
143
+ `observe.mcp_servers`.
144
+
129
145
  Use `agent.list_my_work` for the actor's unified current-work inbox,
130
146
  `observe.work` for account-wide current work, and `observe.activity` for
131
147
  historical events. The older split workflow/expectation work calls are retired.
@@ -0,0 +1,10 @@
1
+ """Start the bundled workbench under Python isolated mode (including Python 3.9)."""
2
+
3
+ from pathlib import Path
4
+ import runpy
5
+ import sys
6
+
7
+
8
+ if __name__ == "__main__":
9
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
10
+ runpy.run_module("airlock_mcp", run_name="__main__")
package/src/mcp.mjs CHANGED
@@ -1,9 +1,15 @@
1
1
  import readline from "node:readline";
2
2
 
3
- import { PROTOCOL_VERSION, airlockPrompt, gettingStartedText } from "./text.mjs";
3
+ import {
4
+ PROTOCOL_VERSION,
5
+ airlockPrompt,
6
+ gettingStartedText,
7
+ nativeAppManagedMcpText,
8
+ } from "./text.mjs";
4
9
  import { WORKBENCH_TOOLS, callWorkbenchTool } from "./workbench.mjs";
5
10
 
6
11
  const GETTING_STARTED_URI = "airlock://getting-started";
12
+ const NATIVE_APP_MANAGED_MCP_URI = "airlock://native-app-managed-mcp";
7
13
 
8
14
  const START_TOOL = {
9
15
  name: "airlock_start",
@@ -32,6 +38,21 @@ export function makeError(id, code, message) {
32
38
  }
33
39
 
34
40
  export function handleMcpRequest(message) {
41
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
42
+ const validId = typeof message?.id === "string" || (typeof message?.id === "number" && Number.isFinite(message.id));
43
+ if (
44
+ !isObject(message) ||
45
+ message.jsonrpc !== "2.0" ||
46
+ typeof message.method !== "string" ||
47
+ !message.method ||
48
+ (Object.hasOwn(message, "id") && !validId) ||
49
+ (Object.hasOwn(message, "params") && !isObject(message.params))
50
+ ) {
51
+ return makeError(validId ? message.id : null, -32600, "invalid request");
52
+ }
53
+ // Notifications have no response and must not dispatch side-effecting tools.
54
+ if (!Object.hasOwn(message, "id")) return undefined;
55
+
35
56
  const { id, method, params } = message;
36
57
 
37
58
  if (method === "initialize") {
@@ -44,10 +65,10 @@ export function handleMcpRequest(message) {
44
65
  },
45
66
  serverInfo: {
46
67
  name: "airlock",
47
- version: "0.1.8",
68
+ version: "0.1.10",
48
69
  },
49
70
  instructions:
50
- "Airlock MCP helps agents improve processes with Airlock specs and build apps or workflows that use existing specs. Use airlock_start for orientation or the airlock_* tools to bootstrap, draft, check, summarize, export, and render specs.",
71
+ "Airlock MCP is the local spec workbench and app-context bootstrap server. Use airlock_start for orientation or the airlock_* tools to bootstrap, draft, check, summarize, export, and render specs. Installed Airlock can separately expose opt-in Snowflake-managed read-only MCP endpoints; read airlock://native-app-managed-mcp for their fixed tools and security boundary.",
51
72
  });
52
73
  }
53
74
 
@@ -122,20 +143,30 @@ export function handleMcpRequest(message) {
122
143
  description: "How to start building and using Airlock specs with Codex.",
123
144
  mimeType: "text/markdown",
124
145
  },
146
+ {
147
+ uri: NATIVE_APP_MANAGED_MCP_URI,
148
+ name: "Installed Airlock Native App MCP",
149
+ description: "Opt-in Snowflake-managed read-only endpoints exposed by an installed Airlock Native App.",
150
+ mimeType: "text/markdown",
151
+ },
125
152
  ],
126
153
  });
127
154
  }
128
155
 
129
156
  if (method === "resources/read") {
130
- if (params?.uri !== GETTING_STARTED_URI) {
157
+ if (params?.uri !== GETTING_STARTED_URI && params?.uri !== NATIVE_APP_MANAGED_MCP_URI) {
131
158
  return makeError(id, -32602, `unknown resource: ${params?.uri || ""}`);
132
159
  }
160
+ const text =
161
+ params.uri === NATIVE_APP_MANAGED_MCP_URI
162
+ ? nativeAppManagedMcpText()
163
+ : gettingStartedText("Home");
133
164
  return makeResponse(id, {
134
165
  contents: [
135
166
  {
136
167
  uri: params.uri,
137
168
  mimeType: "text/markdown",
138
- text: gettingStartedText("Home"),
169
+ text,
139
170
  },
140
171
  ],
141
172
  });
@@ -167,18 +198,13 @@ export async function runServer({ input = process.stdin, output = process.stdout
167
198
  continue;
168
199
  }
169
200
 
170
- if (message.id === undefined) {
171
- handleMcpRequest(message);
172
- continue;
173
- }
174
-
175
201
  try {
176
202
  const response = handleMcpRequest(message);
177
203
  if (response) {
178
204
  output.write(encodeMessage(response));
179
205
  }
180
206
  } catch (error) {
181
- output.write(encodeMessage(makeError(message.id, -32603, error.message)));
207
+ output.write(encodeMessage(makeError(message?.id ?? null, -32603, error.message)));
182
208
  }
183
209
  }
184
210
  }
package/src/text.mjs CHANGED
@@ -56,20 +56,28 @@ access, which specs are read sources, which spec records decisions or actions,
56
56
  and where the app should run. Teach the installed Airlock procedure split:
57
57
  \`airlock.observe.*\` is read-only governance observation,
58
58
  \`airlock.agent.*\` is governed agent work, and \`airlock.admin.*\` is
59
- administrative mutation. Prefer observe payloads such as
59
+ administrative mutation. Installed Airlock may also expose separate opt-in
60
+ Snowflake-managed MCP endpoints for the agent and observer application roles.
61
+ Those endpoints have fixed read-only tool allowlists and preserve the
62
+ authenticated Snowflake principal; they are not this local spec-workbench MCP
63
+ server. They never expose mutation, governed payload rows, restricted-reference
64
+ reads, or attachment bytes. Prefer observe payloads such as
60
65
  \`observe.procedures\`, \`observe.specs\`, \`observe.spec\`,
61
66
  \`observe.governance_map\`, \`observe.explain_access\`, \`observe.health\`,
62
67
  \`observe.activity\`, \`observe.admin_activity\`, \`observe.spec_admin_activity\`,
63
68
  and \`observe.billing_events\` before inventing custom read paths. For \`alter_spec\`
64
69
  activity, use \`CHANGED_SECTIONS\` and \`CHANGED_FIELDS\` to triage what changed
65
- before fetching version snapshots. For polling loops, cache the role-scoped
66
- \`agent.spec_state(...).STATE_TOKEN\` and do larger reads only when it changes.
70
+ before fetching version snapshots. For polling loops, use
71
+ \`agent.spec_state(...).STATE_TOKEN\` for one known spec or
72
+ \`agent.spec_states(...)\` for up to 100 known specs under one role lens, and
73
+ do larger reads only when a token changes.
67
74
  After selecting one logical file, compare
68
75
  \`agent.file_state(...).FILE_STATE_ID\` to detect data, workflow, attachment,
69
76
  exact-reference, or retained version-history changes. Observer principals may use
70
- the authorization-scoped \`observe.spec_state\` and \`observe.file_state\`
71
- equivalents. A missing object or revoked observer grant returns no row as the
72
- terminal polling signal; authorized query failures remain errors. Holding
77
+ the authorization-scoped \`observe.spec_state\`, \`observe.spec_states\`, and
78
+ \`observe.file_state\` equivalents. Missing or inaccessible specs are omitted
79
+ from a plural result without disclosing the cause; that omitted row is the
80
+ terminal polling signal. Authorized query failures remain errors. Holding
73
81
  \`app_observer\` opens only the read-only procedure family; Airlock
74
82
  assignments plus \`observer_access.observer_roles\` determine spec visibility,
75
83
  and account-wide observation requires \`app_admin\` or the locked Airlock role
@@ -164,6 +172,7 @@ Airlock MCP will offer:
164
172
  - spec design with the bundled workbench
165
173
  - Airlock operating patterns for OODA loops and separation of duties
166
174
  - read-only observe procedures for governance maps, health, access explanation, activity, billing events, and context packets
175
+ - the optional role-separated Snowflake-managed MCP endpoints exposed by installed Airlock
167
176
  - scoped spec and file state tokens for efficient polling
168
177
  - app context seeding with spec snapshots and manifests
169
178
  - app and workflow coding against existing Airlock specs
@@ -175,6 +184,78 @@ Airlock MCP will offer:
175
184
  - airlock-specs library patterns as starting points, checked against current artifacts`;
176
185
  }
177
186
 
187
+ export function nativeAppManagedMcpText() {
188
+ return `# Installed Airlock Native App MCP
189
+
190
+ This npm package runs a local spec workbench and app-context bootstrap MCP
191
+ server. An installed Airlock Native App can separately expose two opt-in,
192
+ Snowflake-managed MCP endpoints for governed operational discovery. Enabling
193
+ those endpoints does not turn this local server into a Snowflake client.
194
+
195
+ ## Enable And Inspect
196
+
197
+ An Airlock administrator selects a warehouse and enables each interface
198
+ explicitly. Replace \`airlock\` with the installed application object name when
199
+ it differs:
200
+
201
+ \`\`\`sql
202
+ CALL airlock.admin.enable_mcp_server('agent', 'AGENT_WH', TRUE);
203
+ CALL airlock.admin.enable_mcp_server('agent', 'AGENT_WH');
204
+ CALL airlock.admin.enable_mcp_server('observer', 'OBSERVER_WH');
205
+ CALL airlock.observe.mcp_servers();
206
+ \`\`\`
207
+
208
+ The first call is validate-only. Use
209
+ \`airlock.admin.disable_mcp_server('agent')\` or \`'observer'\` to remove an
210
+ endpoint. Disable and re-enable an endpoint to change its warehouse or refresh
211
+ its fixed tool specification after an Airlock upgrade. The client principal's
212
+ Snowflake role must be able to use the selected warehouse and hold the matching
213
+ Airlock application role.
214
+
215
+ ## Fixed Tool Allowlists
216
+
217
+ The agent endpoint, \`core.airlock_agent_mcp\`, exposes only:
218
+
219
+ - \`airlock_my_access_context\`
220
+ - \`airlock_list_my_work\`
221
+ - \`airlock_describe_spec\`
222
+ - \`airlock_spec_states\`
223
+ - \`airlock_file_state\`
224
+
225
+ The observer endpoint, \`core.airlock_observer_mcp\`, exposes only:
226
+
227
+ - \`airlock_observe_specs\`
228
+ - \`airlock_observe_work\`
229
+ - \`airlock_observe_spec_states\`
230
+ - \`airlock_observe_file_state\`
231
+
232
+ Neither endpoint exposes \`admin.*\`, governed or administrative mutation,
233
+ governed payload rows, restricted-reference reads, or attachment bytes. Existing
234
+ Airlock procedure authorization remains the policy boundary. Except for the
235
+ non-claiming \`airlock_my_access_context\` tool, agent tools retain ordinary
236
+ named-license enforcement and may auto-assign a license when consumer policy
237
+ allows it. Observer tools neither require nor assign a named license.
238
+
239
+ ## Identity And Transport Boundary
240
+
241
+ Every MCP client should authenticate as its own Snowflake principal. A shared
242
+ token, workspace role, channel identity, or external agent id does not create
243
+ Airlock authority. Treat external identity metadata as provenance only; use
244
+ Airlock delegation when one authenticated principal acts for another.
245
+
246
+ The endpoint specifications target scalar \`VARIANT\` adapters in Airlock's
247
+ role-accessible, versioned \`mcp\` transport schema because Snowflake's managed
248
+ MCP SQL API consumes JSONV2 rather than Arrow table results. The adapters call
249
+ the same read-only handlers and retain the same authorization, role-lens,
250
+ observer-scope, and license checks as the public procedures. Only the fixed
251
+ adapters are granted from \`mcp\`; Airlock's private \`internal\` schema remains
252
+ ungranted.
253
+
254
+ Use \`airlock_my_access_context\` first when validating a new agent connection,
255
+ and require that independently issued credentials return their exact distinct
256
+ \`USER_ID\` values.`;
257
+ }
258
+
178
259
  export function gettingStartedText(project) {
179
260
  return `# Airlock MCP
180
261
 
@@ -198,6 +279,13 @@ Airlock MCP gives agents four kinds of Airlock help:
198
279
  context packets, and governance maps before deciding what an app or agent
199
280
  should do.
200
281
 
282
+ Installed Airlock can also expose separate opt-in, role-separated,
283
+ Snowflake-managed read-only MCP endpoints. They preserve the authenticated
284
+ Snowflake principal and use fixed agent and observer tool allowlists. They are
285
+ distinct from this local spec-workbench server. Read
286
+ \`airlock://native-app-managed-mcp\` for the exact tools, lifecycle procedures,
287
+ licensing behavior, and security boundary.
288
+
201
289
  Start in a Git-backed specs repo such as ${specsRepoName(project)}. GitHub is
202
290
  the recommended default when the user has it set up, but any normal repository
203
291
  works. If Codex is creating the repo, ask where the directory should live before
@@ -244,6 +332,11 @@ creates a spec with \`core_config.payload_adapter\` set to
244
332
  \`airlock.admin.load_okf_bundle(...)\`, can sync parsed metadata with
245
333
  \`airlock.admin.sync_okf_bundle_metadata(...)\`, and exposes authoritative
246
334
  accepted context through \`AIRLOCK_DATA.ACTIVE.V_OKF_CONCEPT_METADATA\`.
335
+ The profile supports OKF v0.2 provenance, verification, and lifecycle metadata,
336
+ with legacy v0.1 support. Read the pattern README for examples. Verification
337
+ claims and \`status: stable\` do not grant Airlock approval or permissions.
338
+ Bundles remain bounded Markdown-only packages; loading never executes a
339
+ computation or attester. This MCP workbench does not deploy that Native App update.
247
340
 
248
341
  Give Codex the messy version of the process. Airlock MCP should help turn it
249
342
  into a small first Airlock spec and a plan for more.
@@ -258,13 +351,16 @@ read-side discovery with observe payloads such as \`observe.procedures\`,
258
351
  \`observe.explain_access\`, \`observe.health\`, \`observe.activity\`,
259
352
  \`observe.admin_activity\`, \`observe.spec_admin_activity\`, and \`observe.billing_events\`;
260
353
  for \`alter_spec\` activity, use \`CHANGED_SECTIONS\` and \`CHANGED_FIELDS\` to
261
- triage what changed before fetching version snapshots. For polling loops, poll
262
- \`agent.spec_state\` and compare its scoped \`STATE_TOKEN\` before larger reads;
354
+ triage what changed before fetching version snapshots. For polling loops, use
355
+ \`agent.spec_state\` for one known spec or \`agent.spec_states\` for up to 100
356
+ known specs under one role lens, and compare scoped \`STATE_TOKEN\` values before
357
+ larger reads;
263
358
  compare \`agent.file_state(...).FILE_STATE_ID\` for one selected file. Observer
264
- principals use \`observe.spec_state\` and \`observe.file_state\` for
265
- authorization-scoped read-only state. File-state UUIDs include retained
266
- version-history changes. A missing object or revoked grant returns no row as
267
- the terminal polling signal; authorized query failures remain errors.
359
+ principals use \`observe.spec_state\`, \`observe.spec_states\`, and
360
+ \`observe.file_state\` for authorization-scoped read-only state. File-state
361
+ UUIDs include retained version-history changes. Missing or inaccessible specs
362
+ are omitted from a plural result as the terminal polling signal; authorized
363
+ query failures remain errors.
268
364
  \`app_observer\` grants access to the
269
365
  read-only procedure family; Airlock assignments and
270
366
  \`observer_access.observer_roles\` grant spec visibility. Observe-only calls do
package/src/workbench.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, statSync } from "node:fs";
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
@@ -24,6 +24,9 @@ const workspaceProperty = {
24
24
  };
25
25
 
26
26
  const appModeValues = ["spec-first", "app-first", "co-development"];
27
+ const patternValues = JSON.parse(
28
+ readFileSync(new URL("../patterns/manifest.json", import.meta.url), "utf8"),
29
+ ).patterns.map((pattern) => pattern.name);
27
30
 
28
31
  export const WORKBENCH_TOOLS = [
29
32
  {
@@ -82,7 +85,7 @@ export const WORKBENCH_TOOLS = [
82
85
  {
83
86
  pattern: {
84
87
  type: "string",
85
- enum: ["blank", "okf-knowledge-bundle", "posts"],
88
+ enum: patternValues,
86
89
  description: "Pattern to inspect.",
87
90
  },
88
91
  files: {
@@ -106,7 +109,7 @@ export const WORKBENCH_TOOLS = [
106
109
  },
107
110
  pattern: {
108
111
  type: "string",
109
- enum: ["blank", "okf-knowledge-bundle", "posts"],
112
+ enum: patternValues,
110
113
  description: "Starting pattern. Defaults to blank.",
111
114
  },
112
115
  output: {
@@ -330,12 +333,12 @@ function cliArgsForTool(name, args = {}) {
330
333
  case "airlock_list_patterns":
331
334
  return { cwd, cliArgs: ["list-patterns"] };
332
335
  case "airlock_show_pattern": {
333
- const cliArgs = ["show-pattern", requiredEnum(args, "pattern", ["blank", "posts"])];
336
+ const cliArgs = ["show-pattern", requiredEnum(args, "pattern", patternValues)];
334
337
  if (optionalBoolean(args, "files")) cliArgs.push("--files");
335
338
  return { cwd, cliArgs };
336
339
  }
337
340
  case "airlock_init_workspace": {
338
- const cliArgs = ["init", requiredString(args, "name"), "--pattern", optionalEnum(args, "pattern", ["blank", "posts"], "blank")];
341
+ const cliArgs = ["init", requiredString(args, "name"), "--pattern", optionalEnum(args, "pattern", patternValues, "blank")];
339
342
  maybePush(cliArgs, "--output", optionalString(args, "output", undefined));
340
343
  if (force) cliArgs.push("--force");
341
344
  return { cwd, cliArgs };
@@ -374,15 +377,15 @@ function cliArgsForTool(name, args = {}) {
374
377
  export function runWorkbenchCli(cliArgs, { cwd, env = process.env, spawn = spawnSync } = {}) {
375
378
  const root = packageRoot();
376
379
  const python = env.AIRLOCK_MCP_PYTHON || env.PYTHON || "python3";
377
- const pythonPath = [path.join(root, "src"), env.PYTHONPATH].filter(Boolean).join(path.delimiter);
378
- const result = spawn(python, ["-m", "airlock_mcp", ...cliArgs], {
380
+ // Keep cwd for workspace paths, but never import Python code from that project.
381
+ const launcher = path.join(root, "src", "airlock_mcp", "launcher.py");
382
+ const result = spawn(python, ["-I", launcher, ...cliArgs], {
379
383
  cwd,
380
384
  encoding: "utf8",
381
385
  maxBuffer: OUTPUT_MAX_BUFFER,
382
386
  env: {
383
387
  ...env,
384
388
  AIRLOCK_MCP_HOME: root,
385
- PYTHONPATH: pythonPath,
386
389
  },
387
390
  });
388
391