Skip to content

feat(ai): add optional backend commands#118

Closed
CoderMungan wants to merge 1323 commits into
ActiveMemory:mainfrom
CoderMungan:feat/issue-92-ai-backend
Closed

feat(ai): add optional backend commands#118
CoderMungan wants to merge 1323 commits into
ActiveMemory:mainfrom
CoderMungan:feat/issue-92-ai-backend

Conversation

@CoderMungan

Copy link
Copy Markdown
Member

No description provided.

josealekhine and others added 30 commits May 3, 2026 15:57
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
…nd_yarn/editors/vscode/npm_and_yarn-29ea09b511

chore(deps): Bump uuid from 8.3.2 to removed in /editors/vscode in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the /editors/vscode directory: [postcss](https://github.com/postcss/postcss).


Updates `postcss` from 8.5.8 to 8.5.13
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](postcss/postcss@8.5.8...8.5.13)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Implements sanitization layer (MCP-SAN ActiveMemory#49) and coverage expansion
(MCP-COV ActiveMemory#50) for the MCP server.

Sanitization:
- Add internal/sanitize package: Content, Reflect, SessionID,
  StripControl, truncate helpers
- Add internal/config/sanitize constants (NullByte, DotDot, etc.)
- Add internal/config/regex/sanitize compiled regexes
- Add MaxSourceLimit, MaxContentLen, MaxNameLen, MaxQueryLen,
  MaxCallerLen, MaxURILen constants to internal/config/mcp/cfg
- Add InputTooLong error constructor in internal/err/mcp
- Add mcp.err-input-too-long and mcp.err-unknown-entry-type YAML keys
- Apply sanitize.Content in extract EntryArgs and prompt buildEntry
- Apply sanitize.Reflect for query, caller, name, URI in all
  tool/prompt/resource dispatch paths
- Cap source limit to MaxSourceLimit in tool handler

Coverage:
- internal/entity/mcp_session_test.go: 10 tests covering MCPSession
  lifecycle methods including PendingCount, RecordSessionStart,
  RecordContextLoaded, RecordDriftCheck, RecordContextWrite,
  IncrementCallsSinceWrite
- internal/mcp/server/server_test.go: 15+ new tests covering
  notification handling, subscribe/unsubscribe error paths,
  ctx_remind with active and future-dated reminders, ctx_drift
  missing-file violations, ctx_complete empty and non-matching query
- internal/mcp/proto/schema_test.go: JSON round-trip tests
- internal/mcp/server/def/{tool,prompt}/*_test.go: def count tests
- internal/mcp/server/{extract,io,out,parse,stat}/*_test.go: unit tests

Signed-off-by: CoderMungan <codermungan@gmail.com>
Closes a cross-IDE leak where opening a non-ctx workspace in
Cursor and submitting a single prompt deposited a stub
.context/state/ (mode 0750) into the project. Cursor imports
Claude Code hooks and sets CLAUDE_PROJECT_DIR to the workspace
root for compatibility; the ctx@activememory-ctx plugin's
UserPromptSubmit chain then fired in every workspace. The
check-reminder hook's "provenance-first" ordering called
Preamble -> nudge.Paused -> PauseMarkerPath -> state.Dir
before the Initialized() gate, leaving the mkdir as the
authoritative source of the leak.

Move the gate inside state.Dir() itself: when the project is
not initialized, return errCtx.ErrNotInitialized without
mkdir. Hook callers' existing dirErr != nil branches absorb
the new sentinel silently; interactive callers (ctx add,
ctx task complete, ctx prune) surface a path-bearing message
via cobra's standard error path. Refactor cooldown.TombstonePath
to delegate to state.Dir so the same gate applies to the
PreToolUse "ctx agent" hook path.

Tests:
- internal/cli/system/core/state: full Dir/Initialized matrix
  including override bypass and partial-init cases.
- internal/cli/system/cmd/check_reminder: end-to-end regression
  that simulates the Cursor flow and asserts no .context/ exists
  after running the hook in an uninitialized tempdir.
- pause/resume tests updated to seed the now-required init
  files (FilesRequired) so state.Dir keeps succeeding.

Spec: specs/state-dir-no-mkdir-when-uninitialized.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
…nd_yarn/editors/vscode/npm_and_yarn-0a4fd6b046

chore(deps-dev): Bump postcss from 8.5.8 to 8.5.13 in /editors/vscode in the npm_and_yarn group across 1 directory
…-mkdir-uninit

fix(state): refuse mkdir in uninitialized projects
Bumps the npm_and_yarn group with 1 update in the /editors/vscode directory: [fast-uri](https://github.com/fastify/fast-uri).


Updates `fast-uri` from 3.1.0 to 3.1.2
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](fastify/fast-uri@v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
…nd_yarn/editors/vscode/npm_and_yarn-053c9c4054

chore(deps-dev): Bump fast-uri from 3.1.0 to 3.1.2 in /editors/vscode in the npm_and_yarn group across 1 directory
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
…g-v2

feat(mcp): MCP-SAN + MCP-COV hardening
Follow-up to PR ActiveMemory#76 (MCP-SAN + MCP-COV hardening) addressing three
issues surfaced during post-merge review:

1. truncate() now backs up to a UTF-8 rune boundary via utf8.RuneStart
   so byte-level cuts never produce invalid UTF-8. Reflect() and
   SessionID() delegate to the shared helper.

2. StripControl() now drops U+2028 (LINE SEPARATOR) and U+2029
   (PARAGRAPH SEPARATOR) explicitly. unicode.IsControl does not match
   these (categories Zl/Zp), yet Markdown renderers may treat them
   as line breaks — leaving them in opens a newline injection path
   through reflected content. Constants live in internal/config/sanitize.

3. SanitizedOpts now enforces MaxOptsFieldLen (4 KB) on context,
   rationale, consequence, lesson, and application — secondary
   prose fields that previously had no length cap. Signature
   changes to (entity.EntryOpts, error); the two call sites in
   route/tool/tool.go propagate the error to the MCP client via
   InputTooLong, naming the offending field.

Spec: specs/sanitize-hardening-followup.md
…8-zl-zp-opts-caps

fix(sanitize): UTF-8-safe truncation, Zl/Zp stripping, opts length caps
Phase SK tasks 1 and 2 — tighten the capture surface so
ctx decision add and ctx learning add reject submissions
that lack body content.

Behavior:
- ctx decision add now requires --context, --rationale,
  --consequence to be present and non-placeholder.
- ctx learning add now requires --context, --lesson,
  --application to be present and non-placeholder.
- Placeholder values rejected case-insensitively after trim:
  tbd, n/a, na, none, see chat, see above, see below,
  pending, to be done, plus whitespace-only.
- Substring matches are NOT placeholders ("we deferred the
  rationale as TBD originally" passes; "TBD" alone does not).

Layout follows the project conventions:
- internal/config/validate/ holds the placeholder constants.
- internal/err/cli/ gains FlagEmpty, FlagPlaceholder,
  FlagUnregistered, MarkRequiredFailed; format strings in
  errors.yaml under err.validation.*.
- internal/cli/add/core/validate/ wires required-flag
  marking and the placeholder-rejection PreRunE wrapper.
  The noun-level Cmd() in decision/cmd/add and
  learning/cmd/add calls RequireBodyFlags with the noun's
  three body flags.

Tests cover: each placeholder value, whitespace, substring
acceptance, missing-flag rejection, and PreRunE chaining.

Spec: specs/skill-surface-polish.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Phase SK tasks 3–7 — tighten the skill surface so the capture
and design skills match the rigor of the sibling editorial
pipeline.

- /ctx-spec gains a --brief <path> flag. When supplied, the
  skill reads the file as authoritative and skips the
  interactive Q&A. Authority order when sources disagree:
  frozen docs > recorded DECISIONS > the brief > agent
  inference (labeled TBD). Light compression for clarity is
  allowed; new facts are not.
- /ctx-plan always offers to save the debated brief to
  .context/briefs/<TS>-<slug>.md after the interview. The
  brief is the canonical handoff to /ctx-spec --brief.
- /ctx-decision-add, /ctx-learning-add, /ctx-task-add,
  /ctx-convention-add gain an "Authority boundary (vs other
  skills)" section. Each lists the cross-promotions it
  refuses to perform silently (learning→decision,
  learning→convention, casual remark→task, one-off
  choice→convention, etc.) so promotion only happens on
  explicit user ask.
- The "light compression for clarity is allowed; new facts
  are not" wording is now standardized across the four
  capture skills; the same wording lands in /ctx-handover
  when Phase KB ships.
- docs/reference/skills.md documents the --brief contract
  in the /ctx-spec entry, including the authority order.

Spec: specs/skill-surface-polish.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
…d errors

Earlier f32c8fd wired RequireBodyFlags via panic; the first
attempt at fixing it (in the prior amend of this commit)
replaced the panic with `_ = c.MarkFlagRequired(name)` and
`value, _ := cmd.Flags().GetString(name)` — both silent error
discards. That violates the project's "handle every error"
convention (existing `_ =` discards elsewhere in the codebase
are tech debt, not authorisation to add more).

This refactor takes the right approach: PreRunE is the single
enforcement point. Cobra defaults string flags to "", so the
empty-value check catches missing flags through the same code
path as placeholder rejection. MarkFlagRequired is dropped
entirely (its only contribution was a "(required)" help-text
annotation, which is redundant when PreRunE already enforces).
GetString's error is propagated, not swallowed.

Changes from f32c8fd:
- RequireBodyFlags returns nothing, calls no MarkFlagRequired,
  handles GetString error explicitly via `if getErr != nil`.
- decision/cmd/add and learning/cmd/add stop panicking.
- internal/err/cli loses FlagUnregistered and MarkRequiredFailed
  (unused after this change), plus their DescKeys and yaml
  entries.
- Hook-driven spell corrections (generalising → generalizing,
  standardised → standardized) accepted in the same two
  Authority Boundary blocks.

Spec: specs/skill-surface-polish.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Standardise the author byline on thought-piece posts. Release
recaps and first-person dev stories keep their existing byline.

Signed-off-by: Jose Alekhinne <jose@ctx.ist>
The previous shape of validate.RequireBodyFlags(c, flags...)
silently wrapped the caller's c.PreRunE, saving the prior hook
and chaining to it. That is action-at-a-distance — no other
helper in the codebase mutates a passed-in cobra.Command's
hooks. The caller had no way to know their PreRunE was being
decorated without reading the helper.

Refactor:

- RequireBodyFlags → BodyFlags. Pure function:
  validates each named flag's current value, returns an
  error on the first failure, leaves the command untouched.
- decision/cmd/add and learning/cmd/add install their own
  PreRunE explicitly:
    c.PreRunE = func(cobraCmd *cobra.Command, _ []string) error {
        return validate.BodyFlags(cobraCmd, ...)
    }
  The wiring is visible at the call site.
- Tests rewritten to call BodyFlags directly after parsing
  flags, asserting on pure-function behaviour (acceptance,
  placeholder rejection, missing-flag rejection via the empty
  check, first-failure ordering).

Spec: specs/skill-surface-polish.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
internal/validate/ already exists and its doc.go explicitly
declares itself the anchor for "future non-path validators".
A separate internal/cli/add/core/validate/ duplicated that
philosophy with a tiny pure-function helper. Fold the helper
into internal/validate/ and delete the duplicate.

Changes:
- internal/validate/bodyflags.go — new file with BodyFlags
  and RejectPlaceholder (identical to the deleted helpers).
- internal/validate/bodyflags_test.go — tests moved over.
- internal/validate/doc.go — adds a "CLI Body-Flag Validation"
  section alongside the existing path validators.
- internal/cli/decision/cmd/add/cmd.go and
  internal/cli/learning/cmd/add/cmd.go — import
  internal/validate instead of the old path.
- internal/cli/add/core/validate/ — deleted.

Spec: specs/skill-surface-polish.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Removes internal/validate.BodyFlags entirely. The two callers
(decision add, learning add) now loop their body flags themselves
in PreRunE and call validate.RejectPlaceholder per (flag, value)
pair. The wiring is fully visible at the noun-level constructor
and the validate package becomes uniformly string-consuming
(matches Symlinks, RejectPlaceholder); the cobra.Command-taking
outlier is gone.

The RejectPlaceholder helper moves to its own file
(rejectplaceholder.go / rejectplaceholder_test.go) per the
single-helper-per-file convention used elsewhere in the project
(internal/sanitize/truncate.go). The BodyFlags fixture and tests
that needed a *cobra.Command go away with it; only the four
RejectPlaceholder unit tests remain.

Bundled in this commit because they share the same branch arc
and the repo is mid-rebuild after a Go toolchain bump:

* go.mod bumped 1.26.1 -> 1.26.3 to match the local toolchain
  (the 1.26.1 cached toolchain in ~/go/pkg/mod was corrupt).
* SKILL.md (ctx-spec copilot integration) gains the --brief
  contract section that was already merged into the canonical
  /ctx-spec skill in 55acbd8 but missed the embedded asset.
* Handover note from the prior session is preserved under
  .context/handovers/ following the existing precedent.

Spec: specs/skill-surface-polish.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Captures the localization gap surfaced after 0ccc1a8: the
placeholder set used by RejectPlaceholder is hardcoded English
constants, has no .ctxrc override hook, and uses locale-naive
strings.ToLower that misses Turkish dotted/dotless I and German
sharp s once any non-ASCII placeholder enters the set.

The spec proposes three coordinated moves in one future commit:

1. Move shipped defaults into an embedded YAML asset
   (internal/assets/commands/vocab/placeholders.en.yaml — exact
   path subject to convention audit).
2. Add a .ctxrc placeholders: key with EXTEND semantics, modeled
   on rc.SessionPrefixes() but combining user list onto defaults
   rather than replacing — the dominant case in this codebase is
   "Tarzan Turkish" (EN+TR intermingled), so replace would
   surprise.
3. Replace strings.ToLower with golang.org/x/text/cases.Fold for
   proper Unicode case folding at both ingest and compare time.

Ship en-only in v1; ctx has no locale-specific assets anywhere
yet, so this establishes the structure without committing to a
tr.yaml landing in the same change.

Phase 0 task added with #prerequisite-for-locale-work tag so the
work is identified as gating any future locale-specific phase.

Spec: specs/placeholder-i18n.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Two captures from session 3c81f71b that future contributors need:

* DECISIONS.md: rc.Placeholders() will use EXTEND semantics
  (user list appended to defaults, case-folded de-duplicated),
  diverging from rc.SessionPrefixes() which uses REPLACE. The
  divergence is intentional — Tarzan Turkish (EN+TR intermingled)
  is the dominant case, so REPLACE would force users to re-list
  every English default to add one Turkish term and silently
  regress baseline coverage.

* LEARNINGS.md: the "compile vs go tool version mismatch" error
  is caused by a corrupt cached toolchain in ~/go/pkg/mod/golang.org/
  toolchain@v0.0.1-go<X>.<platform>/, NOT by the system Go install.
  Reinstalling Go does not fix it; deleting the cached dir or
  bumping the go.mod pin does.

Spec: specs/placeholder-i18n.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
OpenCode (opencode.ai) is a terminal-first AI coding agent that reads
AGENTS.md natively and supports MCP servers. This adds `ctx setup opencode`
following the Copilot CLI blueprint: a thin TypeScript plugin embedded as a
static asset that shims OpenCode lifecycle hooks to ctx system subcommands.

Deployed by `ctx setup opencode --write`:
- .opencode/plugins/ctx/index.ts — lifecycle plugin (~35 lines)
- .opencode/plugins/ctx/package.json — minimal dependencies
- opencode.json — MCP server registration (merge-safe)
- AGENTS.md — shared agent instructions
- .opencode/skills/ctx-*/SKILL.md — 4 portable skills

Plugin hooks: session.created (bootstrap), tool.execute.before (dangerous
command blocking), tool.execute.after (post-commit + task completion),
session.idle (persistence nudges), shell.env (CTX_DIR injection).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
…, add tests

The original plugin called `ctx system block-dangerous-commands`, which is
not a real subcommand on the ctx Go binary (it's a Claude-Code plugin-local
hook). On any install without that wrapper Cobra returns exit 1, the
plugin reads that as `{ blocked: true }`, and OpenCode blocks every shell
tool call. Pulling the `tool.execute.before` hook until block-dangerous-
commands is promoted into the Go binary.

Other fixes in the same pass:

- Narrow `post-commit` to actual `git commit` invocations via a regex
  with a negative lookahead so `git commit-tree` / `commit-graph` don't
  trigger it. The previous code ran post-commit after every shell tool.
- Drop the embedded `INSTRUCTIONS.md` asset that nothing read; AGENTS.md
  is what's actually deployed for OpenCode.
- Treat empty / whitespace-only `opencode.json` as "no existing config"
  in `ensureMCPConfig`; previously a pre-created empty file made setup
  hard-error on unmarshal.
- Tighten `extractCommand` to read `{command: string}` shapes instead of
  JSON-stringifying arbitrary input into the dangerous-command pipe.
- Add `mcp_test.go` covering create / empty-file / preserve-keys /
  skip-if-registered / reject-malformed-JSON; add `testmain_test.go`.
- Update user-facing summary text and integration docs to match the
  shipped behavior (drop "blocks dangerous commands" claim, document
  `bun install` step).
- Refresh `specs/opencode-integration.md` to match the landed code and
  record why we deliberately skip `tool.execute.before`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
Persist learnings, decisions, conventions, and follow-up tasks from
the PR ActiveMemory#72 review and refinement pass.

Learnings:
- ctx system help can list project-local Claude wrappers that aren't
  real Go subcommands; non-Claude integrations only see the Go subset
- Trailing \b in a regex matches commit-tree as git commit; need (?!-)
- make test exit code unreliable due to -cover covdata tooling issue

Decisions:
- OpenCode plugin ships without tool.execute.before until
  block-dangerous-commands is a real ctx system Go subcommand
- Editor plugins must filter post-commit to actual git commit calls

Conventions:
- New editor integrations include an MCP-merge test covering the
  five canonical edge cases

Tasks (follow-up):
- Promote block-dangerous-commands to a Go subcommand
- Type-check embedded TS plugin assets in CI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
The plugin callback's first argument is `{tool, sessionID, callID,
args}` per @opencode-ai/plugin v1.4.x. Destructuring `input` pulled
a non-existent property, so the git-commit detection branch and
the EDIT_TOOLS branch never had a real command to inspect — the
post-commit and check-task-completion nudges silently no-op'd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
OpenCode's McpLocalConfig schema (in @opencode-ai/sdk) requires
`command` to be an Array<string> holding both the binary and its
arguments — there's no separate `args` field — and an `enabled`
boolean on the entry. The generator was emitting the Copilot CLI
shape (`command` as a string, `args` as a separate array), so
opencode startup rejected the file with:

  Configuration is invalid at /…/opencode.json
  ↳ Expected array, got "ctx" mcp.ctx.command
  ↳ Missing key mcp.ctx.enabled

Fold mcpServer.Command + Args() into a single command array, set
enabled: true, and drop the args field for the OpenCode path.
The Copilot CLI generator is unchanged — it still uses the
{command, args} split that mcp-config.json expects.

Add KeyEnabled constant; update the MCP regression test to assert
the new shape (command as []string of length 3, no args field,
enabled=true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
…subdirectory)

OpenCode auto-loads only top-level .ts/.js files under
.opencode/plugins/; subdirectories are silently ignored. The
v0.7.x setup deployed the plugin to .opencode/plugins/ctx/index.ts,
so the entire OpenCode integration shipped in PR ActiveMemory#72 — the
session/idle hooks, the post-commit nudge, the check-task
-completion nudge — was never actually loaded by OpenCode. The
file was correct; OpenCode's discovery rule made it dead code.

Verified by smoke-testing both layouts side-by-side:
.opencode/plugins/ctx/index.ts produced no trace events even
with --print-logs --log-level DEBUG. .opencode/plugins/ctx.ts
loaded immediately, factory-call invoked, tool.execute.after
fired with the expected args shape.

Changes:

- internal/cli/setup/core/opencode/plugin.go now writes the
  embedded index.ts content to .opencode/plugins/ctx.ts (flat).
- New cfgHook.FileOpenCodePluginDeploy = "ctx.ts" constant.
  cfgHook.FileIndexTs is kept as the embedded-asset key (the
  source-of-truth filename in the binary) and its docstring now
  spells out the flat-vs-subdir discovery rule for future
  maintainers.
- Drop internal/assets/integrations/opencode/plugin/package.json
  and its //go:embed directive: the plugin uses a type-only
  import of @opencode-ai/plugin (erased at compile time) and the
  host runtime injects PluginInput, so there is no runtime
  dependency tree to install.
- New errSetup.MissingEmbeddedAsset() helper with a matching text
  key, so the new asset lookup uses the err package rather than a
  naked fmt.Errorf (audit fix).
- specs/opencode-integration.md updated to describe the flat
  layout and a smoke-step that verifies a hook actually fires.
- LEARNINGS.md captures the discovery so future plugins for any
  editor verify load before debugging hook contracts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
…context

The MCP server registered by 'ctx setup opencode --write' failed
to hand-shake from OpenCode. Three failure modes, one root cause:
ctx requires CTX_DIR to be absolute (internal/rc.ContextDir's
"absolute-only hardline"), and OpenCode has no path templating
in opencode.json — neither environment.CTX_DIR=".context" nor a
literal absolute path that follows the user's checkout works.

Without an explicit pin, OpenCode forwards the parent shell's
CTX_DIR. A stale value (anchor drift) gives 'context directory
not found'; an unset value with overlapping .context candidates
gives 'multiple candidates visible'. Both kill the JSON-RPC
handshake before any tool can register, leaving 'ctx ✗ failed
MCP error -32000: Connection closed' in 'opencode mcp list'.

Verified: OpenCode launches MCP children with project root as
CWD and forwards parent env (incl. user CTX_DIR). Both confirmed
empirically with a debug shim that logged argv/cwd/env from
inside an opencode mcp list invocation.

Fix: emit ['sh', '-c', 'exec env CTX_DIR="$PWD/.context" ctx mcp
serve']. $PWD is set by sh to the project root OpenCode chose,
giving us an absolute path anchored to whichever checkout owns
this opencode.json. exec replaces the shell so OpenCode's
process tree has ctx directly, no lingering sh layer.

Verified end-to-end: 'opencode mcp list' shows '✓ ctx connected'
and a manual initialize+tools/list handshake against the same
launcher returns the 15 ctx tools.

Changes:

- internal/cli/setup/core/opencode/mcp.go: emit the sh wrapper
  via a new launchCommand() helper; drop the broken
  environment.CTX_DIR field; comment captures the rejection
  reasoning so a future maintainer doesn't reintroduce the
  relative-path attempt.
- internal/cli/setup/core/opencode/mcp_test.go: assert the new
  shape — sh/-c prefix, script substrings (exec env, the quoted
  $PWD/.context expansion, the wrapped invocation), and an
  explicit assertion that 'environment' must NOT be present (the
  failure mode this commit fixes).
- internal/config/shell/shell.go: new CmdFlag ('-c') and
  FormatPOSIXSpawnRelativeCtxDir constants, keeping the
  inline-script template out of call sites per the magic-string
  audit.

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
@CoderMungan

Copy link
Copy Markdown
Member Author

This PR is the full implementation of Block A: AI Backend as specified in
issue #92 (specs/ctx-ai-backend.md).

What ships:

  • internal/backend/Backend interface, Registry, and OpenAI-compatible
    HTTP client
  • internal/backend/vllm.go + openaicompat.go — vLLM and generic
    openai-compatible backends
  • internal/backend/builtin_internal.go — thin wrappers for openai,
    anthropic, ollama, lmstudio
  • internal/cli/ai/ctx ai ping and ctx ai extract commands (Option 1:
    new ctx ai top-level namespace)
  • internal/cli/setup/core/backend/ctx setup --backend <name> extension;
    templates endpoint + auth wiring into .ctxrc
  • internal/rc/.ctxrc backends: YAML table parsing and validation
  • internal/compliance/ai_boundary_test.go — deterministic-core boundary guard;
    CI fails if ctx agent / ctx status / hooks ever import internal/backend
    (structural enforcement of Invariant 2)
  • internal/cli/ai/core/run/ — validation consumer: schema-constrained
    completion → proposal artifact write (.context/*.md files are never touched
    directly)
  • docs/cli/ai.md, docs/cli/setup.md, docs/recipes/local-inference-with-vllm.md
    — CLI reference and recipe docs

Related issues and PRs:

@CoderMungan CoderMungan requested a review from Copilot June 19, 2026 13:25
@CoderMungan CoderMungan requested review from hamzaerbay and v0lkan and removed request for Copilot June 19, 2026 13:27
josealekhine and others added 14 commits June 20, 2026 11:52
Freeze a clean baseline before the snapshot-VM experiment (anchor SDD /
spec-kit alignment). The dream->serendipity engine has landed, but the
v1 workstream is incomplete: add a STATUS banner to the ctx-dream v1
phase making explicit that the end-user dream UX loop is the next task
and the dream-guard consolidation remains open. No code change; do not
assume this phase is shipped when reading the checklist.

Spec: specs/ctx-dream.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
…ndipity-followups

Feat/dream serendipity followups
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
…ub_actions/actions/checkout-7

deps: bump actions/checkout from 6 to 7
…odules/golang.org/x/tools-0.46.0

deps: bump golang.org/x/tools from 0.45.0 to 0.46.0
…and_yarn/editors/vscode/npm_and_yarn-53cbaf2a5b

build(deps-dev): bump esbuild from 0.28.0 to 0.28.1 in /editors/vscode in the npm_and_yarn group across 1 directory
…5 updates

Bumps the npm_and_yarn group with 5 updates in the /editors/vscode directory:

| Package | From | To |
| --- | --- | --- |
| [form-data](https://github.com/form-data/form-data) | `4.0.5` | `4.0.6` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.1.1` | `4.2.0` |
| [markdown-it](https://github.com/markdown-it/markdown-it) | `14.1.1` | `14.2.0` |
| [undici](https://github.com/nodejs/undici) | `7.24.5` | `7.28.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.5` | `8.1.0` |



Updates `form-data` from 4.0.5 to 4.0.6
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](form-data/form-data@v4.0.5...v4.0.6)

Updates `js-yaml` from 4.1.1 to 4.2.0
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](nodeca/js-yaml@4.1.1...4.2.0)

Updates `markdown-it` from 14.1.1 to 14.2.0
- [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md)
- [Commits](markdown-it/markdown-it@14.1.1...14.2.0)

Updates `undici` from 7.24.5 to 7.28.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](nodejs/undici@v7.24.5...v7.28.0)

Updates `vite` from 8.0.5 to 8.1.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.1.0/packages/vite)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: markdown-it
  dependency-version: 14.2.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 7.28.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 8.1.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
…and_yarn/editors/vscode/npm_and_yarn-e0f6736bdb

chore(deps-dev): bump the npm_and_yarn group across 1 directory with 5 updates
@josealekhine

Copy link
Copy Markdown
Member

Thanks for your contributions @CoderMungan

This is the structural enforcement of Invariant 2 ("zero runtime deps for core functionality") and is the strongest thing in the PR.

Security posture — good

  • API key is read from env only, never written to .ctxrc, never logged, never echoed in error bodies:

    // internal/backend/openaicompat_internal.go (headers)
    apiKey := os.Getenv(backend.config.APIKeyEnv)
    if apiKey == "" { return }
    request.Header.Set(cfgBackend.HeaderAuthorization, cfgBackend.AuthorizationBearerPrefix+apiKey)
  • TLS verification on by default (no InsecureSkipVerify); request timeout enforced with a safe 30s fallback; context propagated.

  • Fail-closed registry semantics (no-backend / multiple / missing) match the spec and are tested.

  • Typed error sentinels with Unwrap + externalized message constants — matches repo conventions.

  • The CLI namespace decision (Option 1, ctx ai <verb>) was recorded in DECISIONS.md as the spec's first task required.


Findings worth acting on

1. (functional, headline) Schema-constrained output is plumbed but never sent

Propose passes a schema, but Complete drops it:

// internal/cli/ai/core/run/run.go (Propose)
response, completeErr := resolved.backend.Complete(ctx, backendPkg.Request{
    Prompt: cfgAI.PromptPrefix + emit + token.NewlineLF + string(data),
    Schema: cfgAI.SchemaMinimal,   // <-- set here
})
// internal/backend/openaicompat.go (Complete)
payload := chatRequest{                 // <-- chatRequest has only Model + Messages
    Model:    model,
    Messages: []chatMessage{{Role: cfgBackend.RoleUser, Content: req.Prompt}},
}
// req.Schema is never referenced

chatRequest (types.go) has no response_format/schema field, and TestOpenAICompatibleCompleteSuccess confirms the wire shape is just {model, messages}. So Request.Schema and SchemaMinimal are dead, and propose relies entirely on the prompt prefix coaxing JSON, then:

decoded := map[string]any{}
decodeErr := json.Unmarshal([]byte(response.Text), &decoded)  // fails on ```json fences or any prose

This is the spec's core value prop ("schema-constrained structured outputs", vLLM guided_json). For a validation-only Block A it half-works, but it overclaims. Either wire response_format: {"type":"json_object"} (and vLLM guided_json/extra_body) into chatRequest, or drop Schema/SchemaMinimal and document JSON as best-effort so the type doesn't imply enforcement.

2. (spec deviation) Malformed backend tables warn instead of refuse

The spec's Edge Cases / Validation Rules say:

.ctxrc malformed (missing required key) → Refuse with a clear parse error naming the offending key; do not silently default.

But the implementation downgrades unknown backend sub-keys to a warning, and a test locks that in:

// internal/rc/validate_test.go
func TestValidate_BackendsUnknownNestedField(t *testing.T) {
    data := []byte("backends:\n  vllm:\n    endpoint: http://localhost:8000\n    api_token: nope\n")
    warnings, err := Validate(data)
    if err != nil { t.Fatalf("unexpected error: %v", err) }   // <-- asserts NO error
    if len(warnings) == 0 { t.Fatal("expected warning for unknown backend field") }
}
// internal/rc/validate.go — only two shapes hard-fail; everything else warns
if backendsShapeError(te.Errors) { return nil, decErr }
return te.Errors, nil

So a typo like endpont: is a warning, the field silently doesn't apply, and at runtime the backend loads with an empty endpoint (caught only later at dispatch). It's intentional and tested, but it contradicts the spec. Reconcile one or the other — if backends are meant to be stricter than the repo's general unknown-field tolerance, route their key errors through a sentinel that backendsShapeError treats as fatal.

3. (correctness) Endpoint path is overwritten, not joined

// internal/backend/openaicompat_internal.go (url)
parsed.Path = path   // discards any path already in the configured endpoint

endpoint: https://host/openai → request goes to https://host/v1/models, dropping /openai. That breaks Azure-style and reverse-proxied/subpath OpenAI-compatible deployments. The spec's examples are all bare hosts so it passes today, but it's a real-world footgun. Prefer joining:

parsed.Path = strings.TrimRight(parsed.Path, "/") + path

4. (spec deviation, UX) Setup doesn't validate the endpoint

validate(options) checks only the backend name:

// internal/cli/setup/core/backend/config_internal.go (validate)
if defaultEndpoint(options.Name) == "" && options.Name != cfgBackend.NameOpenAICompatible {
    return setupErr.UnsupportedBackend(options.Name)
}
return nil

The spec says endpoint http(s) is enforced "at setup time," but it's only checked at dispatch (url()). Worse, ctx setup --backend openai-compatible with no --endpoint writes an empty endpoint (that type has no default) and setup accepts it — the user finds out only on the next load/ping. Add a setup-time endpoint presence + scheme check.

5. (hardening) io.ReadAll(response.Body) is unbounded

// internal/backend/openaicompat_internal.go (do)
raw, readErr := io.ReadAll(response.Body)

A misbehaving or compromised backend can return a huge/streamed body and OOM the CLI (and it flows verbatim into Upstream.Body error strings). The backend is user-configured so severity is low, but an io.LimitReader cap is a cheap guard.

6. (couldn't verify here) gosec G107

golangci-lint isn't installed in my environment, so I couldn't run make lint. The diff correctly adds G101 exclusions for the new env-var-name constants:

# .golangci.yml
- linters: [gosec]
  text: "G101"
  path: "internal/config/backend/"

…but http.NewRequestWithContext(ctx, method, endpoint, …) uses a variable URL, which gosec's G107 often flags, and there's no scoped exclusion or //nosec for internal/backend/. If your CI gosec is stricter than what the author ran, this could fail the lint gate — worth a quick confirm. (If it does fire, a path-scoped exclusion or //nosec G107 with a rationale comment is the established pattern here.)


Minor

  • Downstream BASE_URL templating not implemented. The task list calls for templating ANTHROPIC_BASE_URL/OPENAI_BASE_URL into AI-tool configs; setup writes .ctxrc only, and the "env conflict" warning is repurposed to warn when the API-key env var is already set. Reasonable for Block A, but a scope reduction vs. the tasks.
  • Upstream.Error() omits the status code — it includes Body but not StatusCode (the field is stored but not surfaced in the message).
  • Proposal filename collisionTimestampLayout = "20060102T150405Z" is second-resolution, so two propose runs in the same second overwrite each other. Add sub-second precision or a short suffix.
  • propose --emit is neither required nor defaulted — empty emit still dispatches with an empty kinds list.

parlakisik and others added 5 commits June 25, 2026 21:10
…s, CI, cleanup

Resolve the ctx-desktop review feedback on PR ActiveMemory#110:

- markdown links: allowlist URL schemes (https/mailto) before handing a
  link to openUrl, strip the optional `"title"` from the target so it
  can't leak into the href, and re-inline labels; unsafe/relative
  schemes render as plain text instead of a clickable link.
- git spawns: route git_field and discover::git_branch through the
  hardened run_bin (30s timeout, kill/reap, PATH augmentation). Add one
  git_current_branch source of truth so the write/provenance path and
  the workspace scan both omit a detached HEAD rather than record it as
  a branch literally named "HEAD".
- discover: drop SKIP_DIRS dotted entries already covered by the
  starts_with('.') check.
- ContextPacket: wrap copyCommand's clipboard write in try/catch.
- Cargo.toml: real description/authors plus license and repository.
- remove Vite scaffold residue (App.css, vite.svg, tauri.svg,
  react.svg); point the favicon at the ctx icon; fix the window title.
- CI: add a path-scoped ctx-desktop workflow (fmt, then clippy, then
  test, plus tsc/vite build), fmt-first so a formatting-only failure
  fails fast.
- docs: README "Add workspace..." and the full shipped screen list;
  spec gains a shipped-surface section.

CodeQL alerts 9/10 (incomplete <!-- sanitization) are already resolved
on this branch by the stripHtmlComments fixpoint loop.

Spec: specs/ctx-desktop-api-hardening.md
Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
Signed-off-by: CoderMungan <codermungan@gmail.com>
Signed-off-by: CoderMungan <codermungan@gmail.com>
Signed-off-by: CoderMungan <codermungan@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants