artifacty 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/SECURITY.md +49 -0
- package/docs/integrations.md +34 -1
- package/docs/mcp-public-api.md +68 -0
- package/docs/network-sharing.md +4 -0
- package/docs/release-checklist.md +6 -1
- package/docs/threat-model.md +128 -0
- package/package.json +4 -1
- package/src/cli.js +3 -0
- package/src/lib/check.js +29 -2
- package/src/lib/security.js +23 -1
- package/src/mcp-server.js +322 -2
- package/src/server.js +6 -2
package/README.md
CHANGED
|
@@ -175,6 +175,10 @@ artifacty-mcp
|
|
|
175
175
|
```
|
|
176
176
|
|
|
177
177
|
MCP clients can create artifacts with `artifacty_create`. `artifacty_publish` remains as a backwards-compatible alias.
|
|
178
|
+
MCP clients that support resources or prompts can also read `artifacty://recent`,
|
|
179
|
+
`artifacty://artifacts/{id}`, `artifacty://artifacts/{id}/raw{?version}`, and
|
|
180
|
+
`artifacty://schema/v1`, or use prompt templates such as `artifacty_handoff`,
|
|
181
|
+
`artifacty_review`, and `artifacty_release_notes`.
|
|
178
182
|
|
|
179
183
|
Operational commands:
|
|
180
184
|
|
|
@@ -275,14 +279,17 @@ Schema and storage:
|
|
|
275
279
|
- Diagram, component, source snippet, analysis report, table, and media assets use `diagram`, `component`, `snippet`, `analysis-report`, `table`, and `asset` artifact types.
|
|
276
280
|
- Copilot/Cursor examples cover PR reviews, screenshots, demo recordings, and visual evidence bundles.
|
|
277
281
|
- See [docs/artifact-schema-v1.md](docs/artifact-schema-v1.md).
|
|
282
|
+
- See [docs/mcp-public-api.md](docs/mcp-public-api.md) for MCP tools, resources, prompts, and compatibility notes.
|
|
278
283
|
- See [docs/sarif-csv-artifact-plan.md](docs/sarif-csv-artifact-plan.md) for the SARIF/CSV output artifact roadmap.
|
|
279
284
|
|
|
280
285
|
## Security Model
|
|
281
286
|
|
|
282
287
|
- The HTTP server binds to `127.0.0.1` by default.
|
|
283
|
-
- If `ARTIFACTY_API_TOKEN` is set, HTTP API routes require `Authorization: Bearer <token>` or `x-artifacty-token
|
|
288
|
+
- If `ARTIFACTY_API_TOKEN` is set, HTTP API routes require `Authorization: Bearer <token>` or `x-artifacty-token`; scripts should prefer headers over `?token=...` URLs.
|
|
289
|
+
- API token checks use timing-safe digest comparison.
|
|
284
290
|
- Binding outside localhost requires both `ARTIFACTY_SHARE_MODE=lan` or `team` and `ARTIFACTY_API_TOKEN`.
|
|
285
291
|
- Non-local sharing is intended for trusted LAN or VPN sessions. Prefer a specific interface IP over `0.0.0.0`, keep React rendering disabled, and see [docs/network-sharing.md](docs/network-sharing.md).
|
|
292
|
+
- Non-local binding prints a startup warning because Artifacty does not terminate TLS.
|
|
286
293
|
- Artifact content is scanned for common API keys and private keys before storage. Use `--allow-secrets` or `ARTIFACTY_ALLOW_SECRETS=true` only for intentional exceptions.
|
|
287
294
|
- Creates, updates, reads, imports, archives, and restores write audit events to SQLite.
|
|
288
295
|
- CodeMirror editor/viewer and renderer assets are served from local npm dependencies through a package allowlist, not from a public CDN. JavaScript asset routes answer `Origin: null` requests with `Access-Control-Allow-Origin: null` so sandboxed renderer iframes can import local ESM without `allow-same-origin`.
|
|
@@ -295,5 +302,6 @@ Schema and storage:
|
|
|
295
302
|
- CSV artifacts render as an escaped, bounded table; `/raw` preserves the original text.
|
|
296
303
|
- Image and video artifacts store base64 media inline, render safe previews, and decode bytes through `/raw`.
|
|
297
304
|
- Artifact content should still be treated as untrusted; use the raw view when handing content back to an agent.
|
|
305
|
+
- npm releases are published with GitHub Actions OIDC Trusted Publishing after lint, test, and smoke checks pass.
|
|
298
306
|
|
|
299
|
-
See [docs/release-checklist.md](docs/release-checklist.md) before publishing or running a shared instance.
|
|
307
|
+
See [SECURITY.md](SECURITY.md), [docs/threat-model.md](docs/threat-model.md), and [docs/release-checklist.md](docs/release-checklist.md) before publishing or running a shared instance.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Artifacty is pre-1.0. Security fixes are provided for the latest published
|
|
6
|
+
minor release. Upgrade to the latest `artifacty` version before reporting a
|
|
7
|
+
suspected issue.
|
|
8
|
+
|
|
9
|
+
## Reporting a Vulnerability
|
|
10
|
+
|
|
11
|
+
Use GitHub private vulnerability reporting if it is enabled for this repository.
|
|
12
|
+
If private reporting is unavailable, open a GitHub issue with a minimal summary
|
|
13
|
+
and avoid including exploit details, secrets, private artifacts, or sensitive
|
|
14
|
+
network information.
|
|
15
|
+
|
|
16
|
+
Please include:
|
|
17
|
+
|
|
18
|
+
- Artifacty version and install method.
|
|
19
|
+
- Node.js version and operating system.
|
|
20
|
+
- Whether the HTTP server was localhost-only, LAN/team mode, or behind a proxy.
|
|
21
|
+
- Whether `ARTIFACTY_API_TOKEN` and `ARTIFACTY_ENABLE_REACT_RENDERER` were set.
|
|
22
|
+
- Reproduction steps using non-sensitive sample content.
|
|
23
|
+
|
|
24
|
+
## Security Model
|
|
25
|
+
|
|
26
|
+
Artifacty is local-first. The default HTTP server binds to `127.0.0.1`; binding
|
|
27
|
+
outside loopback requires `ARTIFACTY_SHARE_MODE=lan|team` and an API token.
|
|
28
|
+
|
|
29
|
+
The project treats artifact content as untrusted:
|
|
30
|
+
|
|
31
|
+
- HTML, SVG, Mermaid, and React rendering use sandboxed frames and scoped CSP.
|
|
32
|
+
- React execution is disabled unless `ARTIFACTY_ENABLE_REACT_RENDERER=true`.
|
|
33
|
+
- Mutating browser routes reject non-local `Origin` headers.
|
|
34
|
+
- API token checks use constant-time digest comparison.
|
|
35
|
+
- Artifact content is scanned for common API keys and private keys before
|
|
36
|
+
storage unless explicitly allowed.
|
|
37
|
+
|
|
38
|
+
Artifacty does not provide TLS termination, remote OAuth, multi-user access
|
|
39
|
+
control, or a hosted sync service. Do not expose the server directly to the
|
|
40
|
+
public internet.
|
|
41
|
+
|
|
42
|
+
## Supply Chain
|
|
43
|
+
|
|
44
|
+
npm releases are published through GitHub Actions using npm Trusted Publishing
|
|
45
|
+
OIDC. The release workflow runs lint, tests, and smoke checks on supported Node
|
|
46
|
+
versions before publishing.
|
|
47
|
+
|
|
48
|
+
See [docs/threat-model.md](docs/threat-model.md) and
|
|
49
|
+
[docs/network-sharing.md](docs/network-sharing.md) for operational guidance.
|
package/docs/integrations.md
CHANGED
|
@@ -83,7 +83,21 @@ node src/cli.js check
|
|
|
83
83
|
- Cursor: writes project `.cursor/mcp.json` using the Cursor `mcpServers` shape. Pass `--config ~/.cursor/mcp.json` for global Cursor setup.
|
|
84
84
|
- `--dry-run` returns the generated config without writing it.
|
|
85
85
|
- `--timeout <ms>` adjusts Codex `startup_timeout_sec` and Gemini `timeout`. It does not change Claude Code startup behavior; set `MCP_TIMEOUT` before launching Claude Code if you need a larger value there.
|
|
86
|
-
- `check` starts the local MCP server and verifies required tools through
|
|
86
|
+
- `check` starts the local MCP server and verifies required tools, resources, and prompts through MCP discovery methods.
|
|
87
|
+
|
|
88
|
+
## Client Compatibility Matrix
|
|
89
|
+
|
|
90
|
+
| Client | Config shape | Scope | Timeout behavior | Restart requirement | Notes |
|
|
91
|
+
| --- | --- | --- | --- | --- | --- |
|
|
92
|
+
| Claude Code | `.mcp.json` `mcpServers` | project | parent `MCP_TIMEOUT`, default 30s | restart Claude Code | no per-server startup timeout field |
|
|
93
|
+
| Codex | TOML `[mcp_servers.artifacty]` | user or explicit config | `startup_timeout_sec = 30.0` | restart Codex session | Windows cold starts may need the 30s default |
|
|
94
|
+
| Gemini CLI | JSON `mcpServers` | user or project | `timeout: 30000` | reload `/mcp` or restart | `trust` is preserved from installer options |
|
|
95
|
+
| GitHub Copilot in VS Code | `.vscode/mcp.json` `servers` | workspace or user profile | client-managed | reload VS Code MCP server | tools are the primary tested surface |
|
|
96
|
+
| Cursor | `.cursor/mcp.json` `mcpServers` | workspace or global | client-managed | restart/reload Cursor MCP | tools are the primary tested surface |
|
|
97
|
+
|
|
98
|
+
All clients should support MCP tools. Resource and prompt display varies by
|
|
99
|
+
client, so Artifacty's core workflows continue to work through tools even when a
|
|
100
|
+
client does not expose resources or prompts in its UI.
|
|
87
101
|
|
|
88
102
|
Generate a token for protected HTTP routes:
|
|
89
103
|
|
|
@@ -196,6 +210,25 @@ Then run `/mcp` inside Gemini CLI to confirm that the Artifacty tools are connec
|
|
|
196
210
|
|
|
197
211
|
Mutating MCP tools scan content for common API keys and private keys before storage. Pass `allowSecrets: true` only for intentional exceptions.
|
|
198
212
|
|
|
213
|
+
## MCP Resources and Prompts
|
|
214
|
+
|
|
215
|
+
Artifacty also exposes read-only MCP resources:
|
|
216
|
+
|
|
217
|
+
- `artifacty://recent`: recent artifacts with pagination and browser URLs.
|
|
218
|
+
- `artifacty://schema/v1`: Artifact schema v1 Markdown.
|
|
219
|
+
- `artifacty://artifacts/{id}`: artifact JSON with metadata, content, and URLs.
|
|
220
|
+
- `artifacty://artifacts/{id}/raw{?version}`: raw latest or versioned content.
|
|
221
|
+
|
|
222
|
+
Prompt templates:
|
|
223
|
+
|
|
224
|
+
- `artifacty_handoff`: continuation handoff.
|
|
225
|
+
- `artifacty_review`: findings-first code review artifact.
|
|
226
|
+
- `artifacty_test_report`: verification evidence.
|
|
227
|
+
- `artifacty_visual_qa`: screenshot/media/browser evidence.
|
|
228
|
+
- `artifacty_release_notes`: release notes with publish evidence and risks.
|
|
229
|
+
|
|
230
|
+
See [mcp-public-api.md](mcp-public-api.md) for the public MCP surface.
|
|
231
|
+
|
|
199
232
|
## Importing Agent Artifacts
|
|
200
233
|
|
|
201
234
|
Use `artifacty_import` or the CLI `import` command when the artifact was produced by another agent and needs normalization before sharing.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# MCP Public API
|
|
2
|
+
|
|
3
|
+
Artifacty's MCP stdio server is the primary agent-to-agent integration surface.
|
|
4
|
+
The server currently targets MCP protocol `2025-06-18`.
|
|
5
|
+
|
|
6
|
+
## Capabilities
|
|
7
|
+
|
|
8
|
+
`initialize` advertises:
|
|
9
|
+
|
|
10
|
+
- `tools`: Artifact create, import, list, get, update, archive, restore, audit, and info.
|
|
11
|
+
- `resources`: static and dynamic read-only artifact resources.
|
|
12
|
+
- `prompts`: reusable workflow prompt templates.
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
Stable tool names:
|
|
17
|
+
|
|
18
|
+
- `artifacty_create`: create a native Artifacty artifact.
|
|
19
|
+
- `artifacty_publish`: backwards-compatible alias for `artifacty_create`.
|
|
20
|
+
- `artifacty_import`: convert external Claude, Codex, Gemini, Copilot, Cursor, Artifacty, or generic payloads.
|
|
21
|
+
- `artifacty_list`: list artifacts with `query`, `tag`, `sourceAgent`, `includeArchived`, `limit`, and `offset`.
|
|
22
|
+
- `artifacty_get`: read one artifact by `id` and optional `version`.
|
|
23
|
+
- `artifacty_update`: append an immutable version.
|
|
24
|
+
- `artifacty_archive` / `artifacty_restore`: toggle archive state.
|
|
25
|
+
- `artifacty_audit`: list audit events.
|
|
26
|
+
- `artifacty_info`: return local store and browser URL information.
|
|
27
|
+
|
|
28
|
+
Tool schemas use Artifacty schema v1 formats and artifact types. New optional
|
|
29
|
+
properties may be added during 0.x releases; existing names should not be
|
|
30
|
+
renamed without a documented migration.
|
|
31
|
+
|
|
32
|
+
## Resources
|
|
33
|
+
|
|
34
|
+
Static resources:
|
|
35
|
+
|
|
36
|
+
- `artifacty://recent`: JSON list of recent artifacts with pagination and browser URLs.
|
|
37
|
+
- `artifacty://schema/v1`: Markdown reference for Artifacty schema v1.
|
|
38
|
+
|
|
39
|
+
Resource templates:
|
|
40
|
+
|
|
41
|
+
- `artifacty://artifacts/{id}`: JSON artifact metadata, selected version, content, and URLs.
|
|
42
|
+
- `artifacty://artifacts/{id}/raw{?version}`: raw artifact content for latest or specified version.
|
|
43
|
+
|
|
44
|
+
Resources are read-only and may record an audit `read` event for artifact content.
|
|
45
|
+
|
|
46
|
+
## Prompts
|
|
47
|
+
|
|
48
|
+
Prompt names:
|
|
49
|
+
|
|
50
|
+
- `artifacty_handoff`
|
|
51
|
+
- `artifacty_review`
|
|
52
|
+
- `artifacty_test_report`
|
|
53
|
+
- `artifacty_visual_qa`
|
|
54
|
+
- `artifacty_release_notes`
|
|
55
|
+
|
|
56
|
+
Each prompt returns one user message that instructs an agent to create or update
|
|
57
|
+
an Artifacty artifact with discoverable `artifactType`, `sourceAgent`, and tags.
|
|
58
|
+
Prompts accept optional context arguments such as `artifactId`, `goal`, `scope`,
|
|
59
|
+
`target`, or `version`.
|
|
60
|
+
|
|
61
|
+
## Compatibility Notes
|
|
62
|
+
|
|
63
|
+
- The MCP server is local stdio only. Remote MCP auth/OAuth is out of scope.
|
|
64
|
+
- Binary media resources return stored base64 text through MCP; browser `/raw`
|
|
65
|
+
decodes first-class `image` and `video` artifacts into bytes.
|
|
66
|
+
- Clients may display resources and prompts differently. Tools remain the most
|
|
67
|
+
widely supported integration path across Claude, Codex, Gemini, Copilot, and
|
|
68
|
+
Cursor.
|
package/docs/network-sharing.md
CHANGED
|
@@ -30,6 +30,8 @@ The generated token protects HTTP API routes and browser write forms. Prefer the
|
|
|
30
30
|
|
|
31
31
|
Artifacty does not terminate TLS. Do not expose it directly on the public internet. If a shared instance must cross an untrusted network, put it behind a TLS reverse proxy or a private VPN.
|
|
32
32
|
|
|
33
|
+
When Artifacty binds outside loopback, startup output includes a warning that the server is reachable beyond the local machine and that TLS is not provided by Artifacty.
|
|
34
|
+
|
|
33
35
|
## Browser Write Behavior
|
|
34
36
|
|
|
35
37
|
Remote browsers can read shared pages, but write actions are intentionally conservative. Mutating browser routes reject non-local `Origin` headers to reduce CSRF risk. For LAN sharing, prefer API or MCP writes with an explicit token header.
|
|
@@ -39,3 +41,5 @@ Do not relax the origin check just to make remote browser writes easier. A futur
|
|
|
39
41
|
## Renderer Guidance
|
|
40
42
|
|
|
41
43
|
Artifact content is untrusted. HTML, SVG, Mermaid, and React artifacts are rendered with sandboxing and CSP controls, but shared viewing still means content reaches another user's browser. Keep `ARTIFACTY_ENABLE_REACT_RENDERER` disabled for LAN sessions unless every viewer trusts the artifact source.
|
|
44
|
+
|
|
45
|
+
See [threat-model.md](threat-model.md) for the full trust-boundary summary.
|
|
@@ -8,7 +8,7 @@ Use this checklist before publishing or distributing Artifacty.
|
|
|
8
8
|
npm run release:check
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
This runs syntax checks, the full Node test suite, and a local smoke test that starts the HTTP server with token auth enabled, creates an artifact, verifies secret blocking, reads audit logs, writes a backup, and checks MCP tool discovery.
|
|
11
|
+
This runs syntax checks, the full Node test suite, and a local smoke test that starts the HTTP server with token auth enabled, creates an artifact, verifies secret blocking, reads audit logs, writes a backup, and checks MCP tool/resource/prompt discovery.
|
|
12
12
|
|
|
13
13
|
## Packaging
|
|
14
14
|
|
|
@@ -23,7 +23,10 @@ This runs syntax checks, the full Node test suite, and a local smoke test that s
|
|
|
23
23
|
|
|
24
24
|
- Keep the default HTTP bind address at `127.0.0.1`.
|
|
25
25
|
- Require `ARTIFACTY_API_TOKEN` and `ARTIFACTY_SHARE_MODE=lan` or `team` before binding to `0.0.0.0`.
|
|
26
|
+
- Confirm non-loopback startup output includes the LAN/team warning.
|
|
27
|
+
- Prefer `x-artifacty-token` or `Authorization: Bearer <token>` over query tokens in scripts.
|
|
26
28
|
- Review secret-scan bypasses. `--allow-secrets` and `ARTIFACTY_ALLOW_SECRETS=true` should be deliberate and temporary.
|
|
29
|
+
- Review [../SECURITY.md](../SECURITY.md) and [threat-model.md](threat-model.md) when changing auth, rendering, MCP, or network-sharing behavior.
|
|
27
30
|
- Treat artifact HTML and imported agent payloads as untrusted content.
|
|
28
31
|
- Confirm scripted artifact iframes never include `allow-same-origin`.
|
|
29
32
|
- Confirm `/assets/*` and `/vendor/npm/*` JavaScript responses return
|
|
@@ -35,6 +38,8 @@ This runs syntax checks, the full Node test suite, and a local smoke test that s
|
|
|
35
38
|
not prove Mermaid or React rendered inside the iframe.
|
|
36
39
|
- Keep `ARTIFACTY_ENABLE_REACT_RENDERER` disabled by default. Enable it only when the operator accepts arbitrary component execution risk.
|
|
37
40
|
- Confirm parent app CSP does not include `unsafe-eval`; it should appear only on the React frame response CSP.
|
|
41
|
+
- Confirm CodeQL, Scorecard, and Dependabot configuration changes are intentional.
|
|
42
|
+
- Confirm npm publish still uses OIDC Trusted Publishing rather than a long-lived npm token.
|
|
38
43
|
|
|
39
44
|
## Operations
|
|
40
45
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Threat Model
|
|
2
|
+
|
|
3
|
+
Artifacty is a local artifact exchange for LLM agents. It is designed for one
|
|
4
|
+
operator, one local store, and trusted local MCP clients by default.
|
|
5
|
+
|
|
6
|
+
## Assets
|
|
7
|
+
|
|
8
|
+
- Artifact content, including generated code, reports, screenshots, and media.
|
|
9
|
+
- Artifact metadata, tags, audit records, and version history.
|
|
10
|
+
- API tokens and generated startup tokens.
|
|
11
|
+
- Local MCP client configuration files.
|
|
12
|
+
- The Artifacty SQLite database and immutable version files.
|
|
13
|
+
|
|
14
|
+
## Trust Boundaries
|
|
15
|
+
|
|
16
|
+
- **HTTP browser server**: local by default, optionally reachable on LAN/team
|
|
17
|
+
networks when explicitly configured.
|
|
18
|
+
- **MCP stdio server**: local process launched by an MCP client. It inherits the
|
|
19
|
+
local user account's filesystem permissions.
|
|
20
|
+
- **Artifact renderers**: untrusted content is rendered inside browser sandbox
|
|
21
|
+
boundaries where practical.
|
|
22
|
+
- **Storage**: Artifacty stores content under `ARTIFACTY_HOME`; anyone with
|
|
23
|
+
filesystem access to that directory can read artifacts.
|
|
24
|
+
- **npm package**: published through GitHub Actions OIDC after automated checks.
|
|
25
|
+
|
|
26
|
+
## Primary Threats and Controls
|
|
27
|
+
|
|
28
|
+
### Accidental Network Exposure
|
|
29
|
+
|
|
30
|
+
Risk: binding to `0.0.0.0` exposes Artifacty on every interface, including VPNs
|
|
31
|
+
or cloud VM public interfaces.
|
|
32
|
+
|
|
33
|
+
Controls:
|
|
34
|
+
|
|
35
|
+
- Default host is `127.0.0.1`.
|
|
36
|
+
- Non-loopback binding requires `ARTIFACTY_SHARE_MODE=lan|team`.
|
|
37
|
+
- Non-loopback binding also requires `ARTIFACTY_API_TOKEN`.
|
|
38
|
+
- Startup logs warn when the server binds outside loopback.
|
|
39
|
+
|
|
40
|
+
Guidance: prefer a specific private interface IP over `0.0.0.0`. Do not expose
|
|
41
|
+
Artifacty directly to the public internet.
|
|
42
|
+
|
|
43
|
+
### Token Leakage
|
|
44
|
+
|
|
45
|
+
Risk: query-string tokens can appear in browser history, shell history, logs, or
|
|
46
|
+
referrers.
|
|
47
|
+
|
|
48
|
+
Controls:
|
|
49
|
+
|
|
50
|
+
- Scripts should use `x-artifacty-token` or `Authorization: Bearer <token>`.
|
|
51
|
+
- Browser form token URLs exist only for local convenience.
|
|
52
|
+
- Token comparisons use timing-safe digest comparison.
|
|
53
|
+
|
|
54
|
+
Guidance: rotate tokens after sharing sessions and prefer foreground generated
|
|
55
|
+
tokens for temporary LAN use.
|
|
56
|
+
|
|
57
|
+
### Cross-Site Request Forgery
|
|
58
|
+
|
|
59
|
+
Risk: a remote website could attempt to submit writes to an Artifacty server
|
|
60
|
+
reachable from the user's browser.
|
|
61
|
+
|
|
62
|
+
Controls:
|
|
63
|
+
|
|
64
|
+
- Mutating browser routes reject non-local `Origin` headers.
|
|
65
|
+
- API routes require a token when configured.
|
|
66
|
+
- LAN/team mode does not relax browser-origin checks.
|
|
67
|
+
|
|
68
|
+
### Untrusted Artifact Rendering
|
|
69
|
+
|
|
70
|
+
Risk: HTML, SVG, Mermaid, or React content could execute code in a viewer's
|
|
71
|
+
browser.
|
|
72
|
+
|
|
73
|
+
Controls:
|
|
74
|
+
|
|
75
|
+
- HTML artifacts render in sandboxed iframes.
|
|
76
|
+
- SVG artifacts render in scriptless sandboxed iframes after viewer-side
|
|
77
|
+
sanitization.
|
|
78
|
+
- Mermaid renders in a sandboxed iframe without `allow-same-origin`.
|
|
79
|
+
- React is source-only unless `ARTIFACTY_ENABLE_REACT_RENDERER=true`.
|
|
80
|
+
- React rendering, when enabled, runs in a separate sandboxed frame with
|
|
81
|
+
frame-scoped CSP.
|
|
82
|
+
|
|
83
|
+
Guidance: keep React rendering disabled for shared sessions unless all viewers
|
|
84
|
+
trust the source.
|
|
85
|
+
|
|
86
|
+
### Secret Storage
|
|
87
|
+
|
|
88
|
+
Risk: agents may accidentally publish API keys or private keys into artifacts.
|
|
89
|
+
|
|
90
|
+
Controls:
|
|
91
|
+
|
|
92
|
+
- Common API key and private key patterns are scanned before storage.
|
|
93
|
+
- Writes fail unless `allowSecrets` or `ARTIFACTY_ALLOW_SECRETS=true` is set.
|
|
94
|
+
- Stored scan status is recorded in version metadata.
|
|
95
|
+
|
|
96
|
+
Limitations: pattern scanning is best-effort and does not prove content is free
|
|
97
|
+
of sensitive data.
|
|
98
|
+
|
|
99
|
+
### MCP Tool Abuse
|
|
100
|
+
|
|
101
|
+
Risk: an MCP client can create, update, import, archive, restore, and read local
|
|
102
|
+
artifacts through stdio.
|
|
103
|
+
|
|
104
|
+
Controls:
|
|
105
|
+
|
|
106
|
+
- MCP is local stdio only.
|
|
107
|
+
- MCP writes go through the same secret scan and audit paths as CLI/HTTP writes.
|
|
108
|
+
- MCP resources are read-only.
|
|
109
|
+
|
|
110
|
+
Guidance: install Artifacty MCP only in clients and workspaces you trust.
|
|
111
|
+
|
|
112
|
+
## Out of Scope
|
|
113
|
+
|
|
114
|
+
- Public internet hosting without a separate TLS/auth proxy.
|
|
115
|
+
- Multi-user browser write access.
|
|
116
|
+
- OAuth or remote MCP authorization.
|
|
117
|
+
- Per-artifact ACLs.
|
|
118
|
+
- Encrypted-at-rest storage.
|
|
119
|
+
- Malware analysis of arbitrary artifact content.
|
|
120
|
+
|
|
121
|
+
## Security Review Checklist
|
|
122
|
+
|
|
123
|
+
- Run `npm run release:check`.
|
|
124
|
+
- Run `artifacty integrity` against important stores.
|
|
125
|
+
- Confirm non-local sharing uses a token and trusted LAN/VPN.
|
|
126
|
+
- Keep `ARTIFACTY_ENABLE_REACT_RENDERER` disabled unless required.
|
|
127
|
+
- Prefer token headers over query tokens in scripts.
|
|
128
|
+
- Review generated MCP configs before committing workspace files.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "artifacty",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -37,12 +37,15 @@
|
|
|
37
37
|
"docs/artifact-schema-v1.md",
|
|
38
38
|
"docs/assets/artifacty.png",
|
|
39
39
|
"docs/integrations.md",
|
|
40
|
+
"docs/mcp-public-api.md",
|
|
40
41
|
"docs/network-sharing.md",
|
|
41
42
|
"docs/release-checklist.md",
|
|
42
43
|
"docs/sarif-csv-artifact-plan.md",
|
|
44
|
+
"docs/threat-model.md",
|
|
43
45
|
"scripts/smoke.sh",
|
|
44
46
|
"README.md",
|
|
45
47
|
"LICENSE",
|
|
48
|
+
"SECURITY.md",
|
|
46
49
|
"THIRD_PARTY_NOTICES.md",
|
|
47
50
|
"AGENTS.md",
|
|
48
51
|
"CLAUDE.md"
|
package/src/cli.js
CHANGED
|
@@ -68,6 +68,9 @@ async function main() {
|
|
|
68
68
|
});
|
|
69
69
|
process.stderr.write(`Artifacty listening on ${server.url}\n`);
|
|
70
70
|
process.stderr.write(`Store: ${server.store.home}\n`);
|
|
71
|
+
if (server.securityWarning) {
|
|
72
|
+
process.stderr.write(`${server.securityWarning}\n`);
|
|
73
|
+
}
|
|
71
74
|
if (generatedToken) {
|
|
72
75
|
process.stderr.write(`API token: ${generatedToken.token}\n`);
|
|
73
76
|
process.stderr.write(`HTTP header: ${generatedToken.header}\n`);
|
package/src/lib/check.js
CHANGED
|
@@ -14,10 +14,25 @@ export const REQUIRED_MCP_TOOLS = [
|
|
|
14
14
|
"artifacty_info"
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
+
export const REQUIRED_MCP_RESOURCES = [
|
|
18
|
+
"artifacty://recent",
|
|
19
|
+
"artifacty://schema/v1"
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const REQUIRED_MCP_PROMPTS = [
|
|
23
|
+
"artifacty_handoff",
|
|
24
|
+
"artifacty_review",
|
|
25
|
+
"artifacty_test_report",
|
|
26
|
+
"artifacty_visual_qa",
|
|
27
|
+
"artifacty_release_notes"
|
|
28
|
+
];
|
|
29
|
+
|
|
17
30
|
export async function checkMcpTools(options = {}) {
|
|
18
31
|
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
19
32
|
const serverPath = path.resolve(options.serverPath || path.join(projectDir, "src", "mcp-server.js"));
|
|
20
33
|
const requiredTools = options.requiredTools || REQUIRED_MCP_TOOLS;
|
|
34
|
+
const requiredResources = options.requiredResources || REQUIRED_MCP_RESOURCES;
|
|
35
|
+
const requiredPrompts = options.requiredPrompts || REQUIRED_MCP_PROMPTS;
|
|
21
36
|
const timeoutMs = Number(options.timeout || 5000);
|
|
22
37
|
const client = spawnMcpClient({
|
|
23
38
|
serverPath,
|
|
@@ -39,14 +54,26 @@ export async function checkMcpTools(options = {}) {
|
|
|
39
54
|
const listed = await client.request("tools/list", {});
|
|
40
55
|
const toolNames = (listed.tools || []).map((tool) => tool.name).sort();
|
|
41
56
|
const missingTools = requiredTools.filter((tool) => !toolNames.includes(tool));
|
|
57
|
+
const listedResources = await client.request("resources/list", {});
|
|
58
|
+
const resourceUris = (listedResources.resources || []).map((resource) => resource.uri).sort();
|
|
59
|
+
const missingResources = requiredResources.filter((resource) => !resourceUris.includes(resource));
|
|
60
|
+
const listedPrompts = await client.request("prompts/list", {});
|
|
61
|
+
const promptNames = (listedPrompts.prompts || []).map((prompt) => prompt.name).sort();
|
|
62
|
+
const missingPrompts = requiredPrompts.filter((prompt) => !promptNames.includes(prompt));
|
|
42
63
|
|
|
43
64
|
return {
|
|
44
|
-
ok: missingTools.length === 0,
|
|
65
|
+
ok: missingTools.length === 0 && missingResources.length === 0 && missingPrompts.length === 0,
|
|
45
66
|
serverPath,
|
|
46
67
|
protocolVersion: initialized.protocolVersion,
|
|
47
68
|
toolCount: toolNames.length,
|
|
69
|
+
resourceCount: resourceUris.length,
|
|
70
|
+
promptCount: promptNames.length,
|
|
48
71
|
tools: toolNames,
|
|
49
|
-
|
|
72
|
+
resources: resourceUris,
|
|
73
|
+
prompts: promptNames,
|
|
74
|
+
missingTools,
|
|
75
|
+
missingResources,
|
|
76
|
+
missingPrompts
|
|
50
77
|
};
|
|
51
78
|
} finally {
|
|
52
79
|
client.close();
|
package/src/lib/security.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
const TOKEN_HEADER = "x-artifacty-token";
|
|
2
4
|
|
|
3
5
|
const SECRET_PATTERNS = [
|
|
@@ -31,12 +33,23 @@ export function validateServerExposure({ host, config = securityConfig() }) {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
export function exposureWarning({ host, config = securityConfig() }) {
|
|
37
|
+
if (isLoopbackHost(host)) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
return [
|
|
41
|
+
`Warning: Artifacty is listening on ${host} in ${config.shareMode} share mode.`,
|
|
42
|
+
"HTTP is not encrypted by Artifacty; use only a trusted LAN/VPN or place it behind TLS.",
|
|
43
|
+
"Prefer x-artifacty-token or Authorization headers for scripts, and keep React rendering disabled unless every viewer trusts the artifact source."
|
|
44
|
+
].join(" ");
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
export function requireToken({ request, url, body = {}, config = securityConfig() }) {
|
|
35
48
|
if (!config.apiToken) {
|
|
36
49
|
return;
|
|
37
50
|
}
|
|
38
51
|
const provided = extractToken({ request, url, body });
|
|
39
|
-
if (provided
|
|
52
|
+
if (!tokensEqual(provided, config.apiToken)) {
|
|
40
53
|
throw Object.assign(new Error("Artifacty API token required"), {
|
|
41
54
|
code: "AUTH_REQUIRED",
|
|
42
55
|
statusCode: 401
|
|
@@ -87,6 +100,15 @@ export function assertNoSecrets(input = {}, options = {}) {
|
|
|
87
100
|
};
|
|
88
101
|
}
|
|
89
102
|
|
|
103
|
+
export function tokensEqual(provided, expected) {
|
|
104
|
+
const providedText = String(provided ?? "");
|
|
105
|
+
const expectedText = String(expected ?? "");
|
|
106
|
+
const providedDigest = createHash("sha256").update(providedText).digest();
|
|
107
|
+
const expectedDigest = createHash("sha256").update(expectedText).digest();
|
|
108
|
+
return timingSafeEqual(providedDigest, expectedDigest) &&
|
|
109
|
+
Buffer.byteLength(providedText) === Buffer.byteLength(expectedText);
|
|
110
|
+
}
|
|
111
|
+
|
|
90
112
|
export function isLoopbackHost(host) {
|
|
91
113
|
const normalized = String(host || "").toLowerCase();
|
|
92
114
|
return normalized === "localhost" ||
|
package/src/mcp-server.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import readline from "node:readline";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
import {
|
|
4
7
|
ARTIFACT_FORMATS,
|
|
5
8
|
ARTIFACT_TYPES,
|
|
@@ -16,6 +19,7 @@ import { convertAgentArtifact } from "./lib/converters.js";
|
|
|
16
19
|
import { resolvePublicBaseUrl } from "./lib/server-state.js";
|
|
17
20
|
|
|
18
21
|
const PROTOCOL_VERSION = "2025-06-18";
|
|
22
|
+
const PACKAGE_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
19
23
|
const store = createStore();
|
|
20
24
|
|
|
21
25
|
const nativeArtifactInputSchema = {
|
|
@@ -255,6 +259,71 @@ const tools = [
|
|
|
255
259
|
}
|
|
256
260
|
];
|
|
257
261
|
|
|
262
|
+
const resourceTemplates = [
|
|
263
|
+
{
|
|
264
|
+
uriTemplate: "artifacty://artifacts/{id}",
|
|
265
|
+
name: "artifact-by-id",
|
|
266
|
+
title: "Artifact by ID",
|
|
267
|
+
description: "Read an Artifacty artifact with metadata, latest version, content, and browser URLs.",
|
|
268
|
+
mimeType: "application/json"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
uriTemplate: "artifacty://artifacts/{id}/raw{?version}",
|
|
272
|
+
name: "artifact-raw-content",
|
|
273
|
+
title: "Artifact Raw Content",
|
|
274
|
+
description: "Read raw artifact content by ID and optional version.",
|
|
275
|
+
mimeType: "text/plain"
|
|
276
|
+
}
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const prompts = [
|
|
280
|
+
{
|
|
281
|
+
name: "artifacty_handoff",
|
|
282
|
+
title: "Create Artifact Handoff",
|
|
283
|
+
description: "Prepare a concise continuation artifact for another agent.",
|
|
284
|
+
arguments: [
|
|
285
|
+
{ name: "goal", description: "Current goal or handoff objective.", required: false },
|
|
286
|
+
{ name: "artifactId", description: "Existing Artifacty artifact to continue from.", required: false }
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "artifacty_review",
|
|
291
|
+
title: "Create Review Artifact",
|
|
292
|
+
description: "Capture code review findings as a shareable Artifacty artifact.",
|
|
293
|
+
arguments: [
|
|
294
|
+
{ name: "scope", description: "Files, branch, PR, or behavior under review.", required: false },
|
|
295
|
+
{ name: "artifactId", description: "Existing artifact with review context.", required: false }
|
|
296
|
+
]
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "artifacty_test_report",
|
|
300
|
+
title: "Create Test Report Artifact",
|
|
301
|
+
description: "Summarize verification commands, status, failures, and residual risk.",
|
|
302
|
+
arguments: [
|
|
303
|
+
{ name: "goal", description: "Feature or release being verified.", required: false },
|
|
304
|
+
{ name: "artifactId", description: "Existing artifact with implementation context.", required: false }
|
|
305
|
+
]
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "artifacty_visual_qa",
|
|
309
|
+
title: "Create Visual QA Artifact",
|
|
310
|
+
description: "Record browser, screenshot, media, or visual regression evidence.",
|
|
311
|
+
arguments: [
|
|
312
|
+
{ name: "target", description: "URL, artifact ID, or UI surface under visual review.", required: false },
|
|
313
|
+
{ name: "artifactId", description: "Existing visual evidence artifact.", required: false }
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "artifacty_release_notes",
|
|
318
|
+
title: "Create Release Notes Artifact",
|
|
319
|
+
description: "Draft release notes from changed artifacts, tests, and known risks.",
|
|
320
|
+
arguments: [
|
|
321
|
+
{ name: "version", description: "Release version.", required: false },
|
|
322
|
+
{ name: "artifactId", description: "Existing roadmap, checklist, or handoff artifact.", required: false }
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
];
|
|
326
|
+
|
|
258
327
|
const rl = readline.createInterface({
|
|
259
328
|
input: process.stdin,
|
|
260
329
|
crlfDelay: Infinity
|
|
@@ -311,14 +380,21 @@ async function handleRequest(message) {
|
|
|
311
380
|
capabilities: {
|
|
312
381
|
tools: {
|
|
313
382
|
listChanged: false
|
|
383
|
+
},
|
|
384
|
+
resources: {
|
|
385
|
+
subscribe: false,
|
|
386
|
+
listChanged: false
|
|
387
|
+
},
|
|
388
|
+
prompts: {
|
|
389
|
+
listChanged: false
|
|
314
390
|
}
|
|
315
391
|
},
|
|
316
392
|
serverInfo: {
|
|
317
393
|
name: "artifacty",
|
|
318
394
|
title: "Artifacty",
|
|
319
|
-
version: "0.
|
|
395
|
+
version: "0.4.0"
|
|
320
396
|
},
|
|
321
|
-
instructions: "Use Artifacty to create, import, list, read, and
|
|
397
|
+
instructions: "Use Artifacty to create, import, list, read, update, and resource-read local artifacts that other agents can reuse."
|
|
322
398
|
};
|
|
323
399
|
}
|
|
324
400
|
|
|
@@ -335,6 +411,27 @@ async function handleRequest(message) {
|
|
|
335
411
|
return callTool(params.name, params.arguments || {});
|
|
336
412
|
}
|
|
337
413
|
|
|
414
|
+
if (message.method === "resources/list") {
|
|
415
|
+
return listResources();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (message.method === "resources/templates/list") {
|
|
419
|
+
return { resourceTemplates };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (message.method === "resources/read") {
|
|
423
|
+
return readResource(requireParam(message.params, "uri"));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (message.method === "prompts/list") {
|
|
427
|
+
return { prompts };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (message.method === "prompts/get") {
|
|
431
|
+
const params = message.params || {};
|
|
432
|
+
return getPrompt(requireParam(params, "name"), params.arguments || {});
|
|
433
|
+
}
|
|
434
|
+
|
|
338
435
|
throw Object.assign(new Error(`Method not found: ${message.method}`), {
|
|
339
436
|
jsonRpcCode: -32601
|
|
340
437
|
});
|
|
@@ -440,6 +537,177 @@ async function callTool(name, args) {
|
|
|
440
537
|
});
|
|
441
538
|
}
|
|
442
539
|
|
|
540
|
+
async function listResources() {
|
|
541
|
+
const page = await listArtifactsPage(store, { limit: 10 });
|
|
542
|
+
const artifactResources = page.artifacts.flatMap((artifact) => [
|
|
543
|
+
{
|
|
544
|
+
uri: artifactResourceUri(artifact.id),
|
|
545
|
+
name: `artifact:${artifact.id}`,
|
|
546
|
+
title: artifact.title,
|
|
547
|
+
description: `${artifact.sourceAgent} ${artifact.artifactType} artifact, v${artifact.latestVersion}`,
|
|
548
|
+
mimeType: "application/json"
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
uri: artifactRawResourceUri(artifact.id),
|
|
552
|
+
name: `artifact-raw:${artifact.id}`,
|
|
553
|
+
title: `${artifact.title} raw`,
|
|
554
|
+
description: `Raw latest ${artifact.format || "text"} content for ${artifact.id}`,
|
|
555
|
+
mimeType: artifact.contentType || "text/plain"
|
|
556
|
+
}
|
|
557
|
+
]);
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
resources: [
|
|
561
|
+
{
|
|
562
|
+
uri: "artifacty://recent",
|
|
563
|
+
name: "recent-artifacts",
|
|
564
|
+
title: "Recent Artifacts",
|
|
565
|
+
description: "Recent Artifacty artifacts with pagination metadata and browser URLs.",
|
|
566
|
+
mimeType: "application/json"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
uri: "artifacty://schema/v1",
|
|
570
|
+
name: "artifact-schema-v1",
|
|
571
|
+
title: "Artifact Schema v1",
|
|
572
|
+
description: "Artifacty schema v1 reference document.",
|
|
573
|
+
mimeType: "text/markdown"
|
|
574
|
+
},
|
|
575
|
+
...artifactResources
|
|
576
|
+
]
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function readResource(uri) {
|
|
581
|
+
if (uri === "artifacty://recent") {
|
|
582
|
+
const publicBaseUrl = await resolvePublicBaseUrl(store);
|
|
583
|
+
const page = await listArtifactsPage(store, { limit: 20 });
|
|
584
|
+
return resourceText(uri, "application/json", {
|
|
585
|
+
artifacts: page.artifacts.map((artifact) => ({
|
|
586
|
+
...artifact,
|
|
587
|
+
url: `${publicBaseUrl}/artifacts/${encodeURIComponent(artifact.id)}`
|
|
588
|
+
})),
|
|
589
|
+
pagination: {
|
|
590
|
+
total: page.total,
|
|
591
|
+
limit: page.limit,
|
|
592
|
+
offset: page.offset,
|
|
593
|
+
hasMore: page.hasMore,
|
|
594
|
+
nextOffset: page.nextOffset,
|
|
595
|
+
previousOffset: page.previousOffset
|
|
596
|
+
},
|
|
597
|
+
search: page.search
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (uri === "artifacty://schema/v1") {
|
|
602
|
+
return {
|
|
603
|
+
contents: [
|
|
604
|
+
{
|
|
605
|
+
uri,
|
|
606
|
+
mimeType: "text/markdown",
|
|
607
|
+
text: await readFile(path.join(PACKAGE_ROOT, "docs", "artifact-schema-v1.md"), "utf8")
|
|
608
|
+
}
|
|
609
|
+
]
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const parsed = parseArtifactResourceUri(uri);
|
|
614
|
+
if (parsed) {
|
|
615
|
+
const artifact = await getArtifact(store, parsed.id, {
|
|
616
|
+
version: parsed.version,
|
|
617
|
+
audit: mcpAuditContext()
|
|
618
|
+
});
|
|
619
|
+
if (parsed.raw) {
|
|
620
|
+
return {
|
|
621
|
+
contents: [
|
|
622
|
+
{
|
|
623
|
+
uri,
|
|
624
|
+
mimeType: artifact.version.contentType || "text/plain",
|
|
625
|
+
text: artifact.content
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
return resourceText(uri, "application/json", await withUrls(artifact));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
throw Object.assign(new Error(`Unknown resource: ${uri}`), {
|
|
634
|
+
jsonRpcCode: -32602
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function getPrompt(name, args) {
|
|
639
|
+
const definition = prompts.find((prompt) => prompt.name === name);
|
|
640
|
+
if (!definition) {
|
|
641
|
+
throw Object.assign(new Error(`Unknown prompt: ${name}`), {
|
|
642
|
+
jsonRpcCode: -32602
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
description: definition.description,
|
|
648
|
+
messages: [
|
|
649
|
+
{
|
|
650
|
+
role: "user",
|
|
651
|
+
content: {
|
|
652
|
+
type: "text",
|
|
653
|
+
text: promptText(name, args)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
]
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function promptText(name, args = {}) {
|
|
661
|
+
const context = args.artifactId
|
|
662
|
+
? `First read Artifacty resource ${artifactResourceUri(args.artifactId)} and use it as context.`
|
|
663
|
+
: "Use the current session context and any relevant Artifacty resources.";
|
|
664
|
+
const common = `${context}
|
|
665
|
+
|
|
666
|
+
Create or update an Artifacty artifact through artifacty_create, artifacty_import, or artifacty_update. Use concise Markdown unless another format is clearly better. Include sourceAgent, artifactType, and tags so another agent can discover it.`;
|
|
667
|
+
|
|
668
|
+
if (name === "artifacty_handoff") {
|
|
669
|
+
return `${common}
|
|
670
|
+
|
|
671
|
+
Goal: ${args.goal || "Prepare a continuation handoff for the next agent."}
|
|
672
|
+
|
|
673
|
+
Capture: current state, changed files or artifacts, commands run, decisions, blockers, residual risk, and next steps.
|
|
674
|
+
Recommended artifactType: handoff. Recommended tags: handoff, continuation.`;
|
|
675
|
+
}
|
|
676
|
+
if (name === "artifacty_review") {
|
|
677
|
+
return `${common}
|
|
678
|
+
|
|
679
|
+
Review scope: ${args.scope || "Review the current implementation or linked artifact."}
|
|
680
|
+
|
|
681
|
+
Capture findings first, ordered by severity, with file/line references when available. Include open questions and verification gaps.
|
|
682
|
+
Recommended artifactType: code-review. Recommended tags: review.`;
|
|
683
|
+
}
|
|
684
|
+
if (name === "artifacty_test_report") {
|
|
685
|
+
return `${common}
|
|
686
|
+
|
|
687
|
+
Verification goal: ${args.goal || "Summarize test and smoke evidence."}
|
|
688
|
+
|
|
689
|
+
Capture commands, status, failures, environment, manual checks, and what remains untested.
|
|
690
|
+
Recommended artifactType: test-report. Recommended tags: verification, test-report.`;
|
|
691
|
+
}
|
|
692
|
+
if (name === "artifacty_visual_qa") {
|
|
693
|
+
return `${common}
|
|
694
|
+
|
|
695
|
+
Visual target: ${args.target || "Inspect the UI or visual artifact under review."}
|
|
696
|
+
|
|
697
|
+
Capture screenshots/media references, viewport, expected behavior, observed issues, and pass/fail verdict.
|
|
698
|
+
Recommended artifactType: design-option or bundle. Recommended tags: visual, qa.`;
|
|
699
|
+
}
|
|
700
|
+
if (name === "artifacty_release_notes") {
|
|
701
|
+
return `${common}
|
|
702
|
+
|
|
703
|
+
Release version: ${args.version || "next"}
|
|
704
|
+
|
|
705
|
+
Capture highlights, breaking changes, migration notes, tests, known limitations, and publish evidence.
|
|
706
|
+
Recommended artifactType: document. Recommended tags: release-notes.`;
|
|
707
|
+
}
|
|
708
|
+
return common;
|
|
709
|
+
}
|
|
710
|
+
|
|
443
711
|
async function createNativeArtifact(args) {
|
|
444
712
|
return createArtifact(store, {
|
|
445
713
|
title: args.title,
|
|
@@ -471,6 +739,49 @@ async function withUrls(artifact) {
|
|
|
471
739
|
};
|
|
472
740
|
}
|
|
473
741
|
|
|
742
|
+
function resourceText(uri, mimeType, data) {
|
|
743
|
+
return {
|
|
744
|
+
contents: [
|
|
745
|
+
{
|
|
746
|
+
uri,
|
|
747
|
+
mimeType,
|
|
748
|
+
text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
|
|
749
|
+
}
|
|
750
|
+
]
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function artifactResourceUri(id) {
|
|
755
|
+
return `artifacty://artifacts/${encodeURIComponent(id)}`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function artifactRawResourceUri(id, version) {
|
|
759
|
+
const suffix = version ? `?version=${encodeURIComponent(String(version))}` : "";
|
|
760
|
+
return `artifacty://artifacts/${encodeURIComponent(id)}/raw${suffix}`;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function parseArtifactResourceUri(uri) {
|
|
764
|
+
let parsed;
|
|
765
|
+
try {
|
|
766
|
+
parsed = new URL(uri);
|
|
767
|
+
} catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (parsed.protocol !== "artifacty:" || parsed.hostname !== "artifacts") {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
775
|
+
if (parts.length !== 1 && !(parts.length === 2 && parts[1] === "raw")) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
id: decodeURIComponent(parts[0]),
|
|
780
|
+
raw: parts[1] === "raw",
|
|
781
|
+
version: parsed.searchParams.get("version") || undefined
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
474
785
|
function toolResult(data) {
|
|
475
786
|
return {
|
|
476
787
|
content: [
|
|
@@ -493,6 +804,15 @@ function requireArg(args, name) {
|
|
|
493
804
|
return args[name];
|
|
494
805
|
}
|
|
495
806
|
|
|
807
|
+
function requireParam(params = {}, name) {
|
|
808
|
+
if (!params[name]) {
|
|
809
|
+
throw Object.assign(new Error(`Missing required parameter: ${name}`), {
|
|
810
|
+
jsonRpcCode: -32602
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return params[name];
|
|
814
|
+
}
|
|
815
|
+
|
|
496
816
|
function writeResponse(id, result, error) {
|
|
497
817
|
const response = {
|
|
498
818
|
jsonrpc: "2.0",
|
package/src/server.js
CHANGED
|
@@ -19,7 +19,7 @@ import { convertAgentArtifact } from "./lib/converters.js";
|
|
|
19
19
|
import { createLineDiff } from "./lib/diff.js";
|
|
20
20
|
import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorClientFilePath, editorVendorPath, viewerClientFilePath } from "./lib/editor-assets.js";
|
|
21
21
|
import { localeFromBodyOrUrl, localeFromUrl, localizedHref } from "./lib/i18n.js";
|
|
22
|
-
import { requireToken, securityConfig, validateServerExposure } from "./lib/security.js";
|
|
22
|
+
import { exposureWarning, requireToken, securityConfig, validateServerExposure } from "./lib/security.js";
|
|
23
23
|
import { writeServerState } from "./lib/server-state.js";
|
|
24
24
|
import { generateToken } from "./lib/token.js";
|
|
25
25
|
import {
|
|
@@ -78,6 +78,7 @@ export async function startServer(options = {}) {
|
|
|
78
78
|
requestedPort,
|
|
79
79
|
port: actualPort,
|
|
80
80
|
portFallback: usedPortFallback,
|
|
81
|
+
securityWarning: exposureWarning({ host, config: security }),
|
|
81
82
|
close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))
|
|
82
83
|
};
|
|
83
84
|
}
|
|
@@ -674,12 +675,15 @@ async function runServerMain(options) {
|
|
|
674
675
|
throw new Error("Use either --api-token or --generate-token, not both");
|
|
675
676
|
}
|
|
676
677
|
const generatedToken = options.generateToken ? generateToken(options) : null;
|
|
677
|
-
const { url, store } = await startServer({
|
|
678
|
+
const { url, store, securityWarning } = await startServer({
|
|
678
679
|
...options,
|
|
679
680
|
apiToken: generatedToken?.token || options.apiToken
|
|
680
681
|
});
|
|
681
682
|
process.stderr.write(`Artifacty listening on ${url}\n`);
|
|
682
683
|
process.stderr.write(`Store: ${store.home}\n`);
|
|
684
|
+
if (securityWarning) {
|
|
685
|
+
process.stderr.write(`${securityWarning}\n`);
|
|
686
|
+
}
|
|
683
687
|
if (generatedToken) {
|
|
684
688
|
process.stderr.write(`API token: ${generatedToken.token}\n`);
|
|
685
689
|
process.stderr.write(`HTTP header: ${generatedToken.header}\n`);
|