Skip to content
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
- Preserve the single global `tasks` array as the source of truth.
- Use a single `renderAll()` integration path for user-visible rerenders.
- Prefer browser-native APIs only.
- Keep `GET /api/health` as process liveness. Report optional Clearfolio
readiness separately (`capability.readiness` logs, `GET /api/capabilities`,
and attachment 503). Do not turn an unconfigured document viewer into a
planner outage.

## Verification

Expand Down
5 changes: 5 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,8 @@
unless `CLEARFOLIO_ARTIFACT_ORIGINS` lists additional exact HTTPS origins.
Cross-origin `artifactToken` values stay on the returned origin and are
never transplanted into the Clearfolio viewer.
- Optional Clearfolio conversion is a replaceable MSA capability. Process
liveness stays on `GET /api/health`. Configuration readiness is emitted at
startup, queried from authenticated `GET /api/capabilities`, and used to
fail attachment upload/view closed with HTTP 503. The in-memory adapter
exists only behind `SCOPEWEAVE_DEV=1`.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
operators can distinguish configured provider, explicit development mock, and
unavailable/invalid configuration without coupling optional document-viewer
readiness to whole-process `/api/health` liveness.
- Added authenticated `GET /api/capabilities` and HTTP 503 attachment
short-circuit so planners see a concrete next action before upload and
operators can query the same record without reading container logs.
- Added deterministic PM analysis for requirements/RFI/RFP readiness, WBS
estimation coverage, dependency risk, and procurement package section checks.
- Preserved PM-analysis research papers, NASA WBS handbook, BCP 14, and JSON
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ deploy guide is `docs/deploy.md`.
- `server.mjs` — `@hono/node-server` entry (PORT, default 8787), serves the API and
the static client via a strict allowlist.
- `app.mjs` — Hono routes (auth/SSO, projects, teams, billing, webhooks, baselines,
revisions, comments, search…); `auth.mjs` — scrypt + pinned-HS256 JWT + PAT hashing;
revisions, comments, search, authenticated capability readiness…); `auth.mjs` — scrypt + pinned-HS256 JWT + PAT hashing;
`billing.mjs` — plans/caps, Stripe via dynamic import; `db.mjs` — `node:sqlite`.
- Only two runtime dependencies (`hono`, `@hono/node-server`); everything else is
Node built-ins. Do not add runtime dependencies (repository contract in
Expand Down
39 changes: 38 additions & 1 deletion cloud-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ export function routeTokenPathSegment(value) {
return ROUTE_TOKEN_RE.test(token) ? token : '';
}

/**
* Return the next action a planner should take when Clearfolio is locally unavailable.
*
* The server remains the authority: this helper only formats an already-safe
* capability record so the attachments dialog can tell the user what to do
* before they pick a file. Empty string means conversion may proceed.
*
* @param {{ready?:boolean,action?:string|null}|null|undefined} capability - Authenticated Clearfolio capability record.
* @returns {string} Concrete next action, or an empty string when upload may continue.
*/
export function clearfolioCapabilityNotice(capability) {
if (!capability || capability.ready) return '';
const action = typeof capability.action === 'string' ? capability.action.trim() : '';
return action
? `문서 변환을 사용할 수 없습니다. ${action}`
: '문서 변환을 사용할 수 없습니다. 운영자에게 Clearfolio 설정을 요청하십시오.';
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

function safeApiPath(path) {
if (typeof path !== 'string' || !path.startsWith('/api/')) throw new Error('invalid api path');
const origin = typeof location !== 'undefined' ? location.origin : 'http://localhost';
Expand Down Expand Up @@ -1196,6 +1214,21 @@ async function openAttachmentsModal() {
head.append(h2, close);
panel.appendChild(head);

let capability = null;
try {
capability = (await api('/api/capabilities'))?.capabilities?.clearfolio || null;
} catch {
capability = null;
}
Comment thread
seonghobae marked this conversation as resolved.
const noticeText = clearfolioCapabilityNotice(capability);
if (noticeText) {
const notice = document.createElement('p');
notice.className = 'capability-notice';
notice.setAttribute('role', 'status');
notice.textContent = noticeText;
panel.appendChild(notice);
}
Comment thread
seonghobae marked this conversation as resolved.

// 작업 선택 + 파일 업로드
const sel = document.createElement('select');
sel.className = 'cloud-select';
Expand All @@ -1220,7 +1253,11 @@ async function openAttachmentsModal() {
const up = document.createElement('button');
up.type = 'submit';
up.className = 'primary-button';
up.textContent = '업로드';
up.textContent = noticeText ? '변환 설정 필요' : '업로드';
if (noticeText) {
fi.disabled = true;
up.disabled = true;
}
form.append(fi, up);
panel.appendChild(form);

Expand Down
15 changes: 9 additions & 6 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,17 @@ credentials never reach the browser. HWP/HWPX are rejected (Clearfolio policy).

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/api/projects/:id/attachments` | multipart `file` (+`taskId?`, ≤10MB) → conversion job (write roles) |
| `GET` | `/api/capabilities` | Authenticated optional-capability readiness (`clearfolio.ready/mode/reason/action`). Configuration-only; no provider I/O. |
| `POST` | `/api/projects/:id/attachments` | multipart `file` (+`taskId?`, ≤10MB) → conversion job (write roles). Unconfigured/invalid Clearfolio returns `503` with the same capability record. |
| `GET` | `/api/projects/:id/attachments?taskId=` | List (+ refreshes pending statuses) |
| `GET` | `/api/projects/:id/attachments/:aid/view` | 302 → signed artifact URL (`?token=` for new-tab opens) |
| `GET` | `/api/projects/:id/attachments/:aid/view` | 302 → signed artifact URL (`?token=` for new-tab opens). Locally unready Clearfolio returns `503`. |
| `DELETE` | `/api/projects/:id/attachments/:aid` | Uploader or manage |

Env: `CLEARFOLIO_URL` plus `CLEARFOLIO_HMAC_SECRET` for production conversion.
Optional `CLEARFOLIO_ARTIFACT_ORIGINS` adds reviewed HTTPS CDN/object-store
origins; unset trusts only the Clearfolio origin. An unset URL is not a
successful converter: the in-memory mock exists only with `SCOPEWEAVE_DEV=1`.
Optional `CLEARFOLIO_ARTIFACT_ORIGINS` adds reviewed exact HTTPS CDN/object-store
origins; unset trusts only the Clearfolio origin. An unset URL in production
makes the capability unavailable (`ready=false`) rather than simulating success;
`SCOPEWEAVE_DEV=1` without a URL enables the in-memory adapter for local work only.

## Comments (코멘트)

Expand Down Expand Up @@ -182,7 +184,8 @@ const ok = req.headers['x-scopeweave-signature'] ===
| `GET` | `/api/orgs/:id/audit` | Audit log (manage; `?format=csv` for a compliance CSV) |
| `GET` | `/api/orgs/:id/export` | Full workspace export JSON (owner) |
| `GET` | `/api/metrics` | Ops counters (JSON; add `?format=prometheus` for scrape-ready text) |
| `GET` | `/api/health` | Liveness |
| `GET` | `/api/capabilities` | Authenticated optional-capability readiness (Clearfolio configuration only) |
| `GET` | `/api/health` | Liveness (`{"ok":true}` even when Clearfolio is unavailable) |

## Example

Expand Down
17 changes: 10 additions & 7 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Outside explicit `SCOPEWEAVE_DEV=1`, Clearfolio operations fail closed with a
stable configuration error and the mock artifact route is not registered. Other
ScopeWeave planning capabilities remain available. For local integration work,
`SCOPEWEAVE_DEV=1` permits the in-memory adapter when the URL is absent and also
permits HTTP only for `localhost`, `127.0.0.1`, or `::1`; remote HTTP endpoints
permits HTTP only for `localhost`, `127.0.0.1`, or `[::1]`; remote HTTP endpoints
are rejected.

At process startup ScopeWeave emits one structured, non-secret readiness record:
Expand All @@ -72,12 +72,15 @@ invalid artifact-origin allowlist report `ready=false` with a stable reason and
a safe remediation instruction.

`GET /api/health` remains liveness-only and returns `{"ok":true}` even when the
optional Clearfolio capability is unavailable. This separation prevents an
optional document-viewer dependency from causing the planner process to be
restarted or removed from service. Kubernetes documents liveness as the signal
for restarting unhealthy containers and readiness as the signal for whether a
container should receive traffic; ScopeWeave keeps the whole application live
while reporting the optional capability independently.
optional Clearfolio capability is unavailable. Authenticated
`GET /api/capabilities` returns the same non-secret record so operators do not
need container logs. Attachment upload and view return HTTP 503 with that
record when the capability is locally unready, before any provider call. This
separation prevents an optional document-viewer dependency from causing the
planner process to be restarted or removed from service. Kubernetes documents
liveness as the signal for restarting unhealthy containers and readiness as the
signal for whether a container should receive traffic; ScopeWeave keeps the
whole application live while reporting the optional capability independently.

Provider URLs are treated as service origins, not arbitrary request prefixes.
Keep credentials in the dedicated HMAC secret setting rather than URL userinfo,
Expand Down
67 changes: 67 additions & 0 deletions docs/doctoring/clearfolio-capability-operator-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Clearfolio capability operator and planner surface

## Decision

Startup logs are not a buyer-usable control surface. A planner who opens 산출물
must learn that document conversion is unavailable before selecting a file, and
an operator who cannot read container stdout must still retrieve the same
non-secret readiness record.

ScopeWeave therefore exposes authenticated `GET /api/capabilities` and fails
attachment upload/view with HTTP 503 when Clearfolio is locally unready. Both
surfaces reuse `clearfolioCapabilityStatus()` and never call the provider.
`GET /api/health` remains liveness-only.

This slice does not claim remote Clearfolio reachability. It completes the
operator/planner half of issue #489 configuration readiness.

## Planner next action

The attachments dialog reads `/api/capabilities` after login. When
`ready=false`, it shows the server `action` as a status notice, disables the
file input, and changes the submit label to `변환 설정 필요`. If the advisory
query fails, the dialog stays open and the server remains authoritative: an
unconfigured upload still returns 503 with the same reason and action.

## Why 503 instead of 502

RFC 9110 distinguishes a gateway/proxy error (502) from a service that is
temporarily or locally unable to handle the request (503). An unconfigured or
unsafe Clearfolio deployment is not a failed downstream hop; it is a local
capability that the process has already decided it cannot serve. Returning 503
with a stable reason prevents operators from paging a remote provider that was
never contacted.

## Security and privacy boundary

- Anonymous callers receive `401` and learn only that the route is
authenticated. Deployment mode (`development_mock` vs `unavailable`) is not
published on unauthenticated surfaces, including `/api/metrics`.
- The JSON body contains only capability name, readiness, mode, stable reason,
and fixed remediation text. HMAC material, URLs, tenant claims, job IDs, and
provider bodies are omitted.
- The UI helper only formats an already-safe record; it does not invent a
second readiness evaluator.

## Verification contract

`tests/unit/clearfolio-capability-readiness.test.mjs` continues to launch a
fresh process per configuration and replaces `fetch` with a throwing function.
Added cases prove HMAC-invalid readiness, authenticated capability query, and
503 upload rejection without provider traffic.
`tests/unit/cloud-sync-security.test.mjs` locks the planner notice copy.

## Rollback

Remove `GET /api/capabilities`, the 503 attachment short-circuit, and the
attachments-dialog notice together. Do not restore implicit production mocks or
make `/api/health` fail.

## References

Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110;
STD 97). Internet Engineering Task Force. https://doi.org/10.17487/RFC9110

The Kubernetes Authors. (2026). *Liveness, readiness, and startup probes*.
Kubernetes Documentation.
https://kubernetes.io/docs/concepts/workloads/pods/probes/
9 changes: 6 additions & 3 deletions docs/doctoring/clearfolio-capability-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Clearfolio is an optional ScopeWeave MSA capability. Its local configuration state must be visible to an operator without turning the whole planner process unhealthy and without making a provider network request merely to answer a health question.

ScopeWeave therefore keeps `GET /api/health` as whole-process liveness and publishes one non-secret structured `capability.readiness` record for Clearfolio at server startup. The readiness record is produced by the same configuration validator used by production Clearfolio operations and returns only four bounded fields: `ready`, `mode`, `reason`, and `action`.
ScopeWeave therefore keeps `GET /api/health` as whole-process liveness and publishes the same non-secret Clearfolio capability record in three operator/user surfaces: one structured `capability.readiness` log at server startup, authenticated `GET /api/capabilities`, and a 503 attachment response that repeats `ready`, `mode`, `reason`, and `action`. The record is produced by the same configuration validator used by production Clearfolio operations.

This is a bounded follow-up slice of issue #489. It does not claim remote Clearfolio reachability, latency, authentication success, artifact availability, or end-to-end readiness. Those require operational evidence from real provider calls and the attachment status path; the startup record proves configuration readiness only.

Expand Down Expand Up @@ -45,7 +45,7 @@ Examples include:

Kubernetes distinguishes liveness from readiness: a failed liveness probe can trigger container restart, while readiness controls whether a workload should receive service traffic. Clearfolio is not required for planning, authentication, project CRUD, or the static client, so treating its configuration as whole-process liveness would turn an optional dependency failure into an unnecessary planner outage.

The existing `/api/health` response remains `{"ok":true}` while the Clearfolio capability is unavailable. Operators inspect the startup readiness record for the optional integration and continue to use attachment failure/status evidence for remote operational diagnosis.
The existing `/api/health` response remains `{"ok":true}` while the Clearfolio capability is unavailable. Operators inspect the startup readiness record or `GET /api/capabilities` for the optional integration. Planners see the same next action in the attachments dialog before they pick a file, and attachment upload/view fail closed with HTTP 503 instead of attempting provider traffic. Remote operational diagnosis still uses attachment failure/status evidence after a valid provider is configured.

RFC 9110 defines a successful GET response as a representation of the target resource state. ScopeWeave keeps the `/api/health` resource narrowly defined as process liveness rather than silently changing its semantics to aggregate every optional dependency.

Expand All @@ -68,7 +68,10 @@ Unknown non-configuration exceptions are rethrown instead of being silently misc
- explicit development mock with a production-configuration action;
- valid production provider configuration;
- insecure production HTTP configuration;
- malformed artifact-origin policy detected before provider transport.
- malformed artifact-origin policy detected before provider transport;
- weak HMAC configuration with a secret-free next action;
- anonymous `GET /api/capabilities` rejected while authenticated callers receive the same local record;
- unconfigured production attachment upload returning HTTP 503 before any provider call.

The regression executes in both `test:unit` and `test:coverage:cases`; `server/clearfolio.mjs` remains in the canonical owned-production c8 target set.

Expand Down
Loading