Skip to content

v0.11.0

Choose a tag to compare

@V3RON V3RON released this 22 Sep 11:23
· 34 commits to main since this release
  • Docs: designing tools for agents. docs/TOOLS.md gains a "Designing tools for agents"
    section (name by intent, annotate, pair mutations with observers, declare outputSchema,
    describe parameters, coarse over fine, timeoutMs, no tools that wait on a person), linked from
    the React Native, iOS and Android READMEs and the website. The Appduct skill is split into a short
    SKILL.md (the CLI loop, chaining calls in one shell invocation, errors) plus on-demand
    references for the CLI, writing tools and setup. The playground tools now follow the rules:
    reset_counter is also idempotentHint, throwing_tool is readOnlyHint, and descriptions
    name their side effects.

  • New: tool groups. A tool can declare an optional group — a top-level group ("cart")
    or one subgroup below it ("checkout/payment"); each part matches the tool-name pattern
    [a-zA-Z0-9_-]{1,64}. Set it with registerTool/useAppductTool's group option (a change
    to it re-registers the tool), with createToolGroup("cart") to bind one group for a whole
    feature module, or with the group parameter of the Swift and Kotlin register calls. An
    invalid group invalidates the registry snapshot exactly like an invalid timeout_ms, so all
    three SDKs reject it at registration — an explicit null included, so an ungrouped tool omits
    the option rather than passing null. A daemon that predates groups ignores the field.

  • New: appduct tools --group <name> and appduct tools --groups. --group checkout lists
    the checkout group and all its subgroups (checkout/payment lists just that subgroup) and
    combines with --filter/--limit/--offset; --groups lists only the groups and their tool
    counts. Without --group, a registry with groups is listed under group headings, and a
    truncated listing's footer names the top-level groups to narrow to. tools.list gains a
    group param (applied before total and paging) and a groups summary of the whole registry
    on every result, so appduct tools --json now returns { tools, total, groups }. Every entry
    carries a group, null for an ungrouped tool — the same value the groups summary uses for
    its own ungrouped row, so one test answers "ungrouped" in either half of the result.

  • New: groups over MCP. appduct_list_tools takes a group (same matching as --group),
    shows each tool's group, and returns the groups summary on every result, so an agent can
    see an app's areas and list one of them. appduct_describe_tool includes the tool's group.

  • Fixed (iOS): a tool name with a trailing newline ("tool\n") is now rejected, matching
    @appduct/shared and Android. The Swift core's name check accepted it because ICU's $ also
    matches before a final line terminator; the daemon would then have rejected the snapshot.

  • Breaking (MCP): the app's tools are no longer listed as MCP tools. An agent reaches them
    through three built-ins that mirror the CLI: appduct_list_tools (one-line signatures and
    each tool's policy, with filter/limit/offset, like appduct tools),
    appduct_describe_tool (one tool's full schema, like appduct tools <name>) and
    appduct_call_tool ({ selector?, name, args?, timeoutMs? }, like appduct invoke).
    tools/list is now a fixed set of built-ins, so an app with hundreds of tools adds three
    definitions to an agent's context, not hundreds. appduct_list_tools returns 50 tools at a time
    unless given limit. selector takes a session alias or id; a call is routed by session id, so
    it fails with unknown_session rather than reaching a new device that took over a departed
    device's alias. Unknown parameters are rejected (invalid_request). timeoutMs can only
    shorten the tool's own deadline, since the app stops a tool at its declared timeout; a longer
    one, or one outside 1000–600000, is rejected rather than clamped. What goes away:

    • Calling an app tool by its own name through tools/call. It now returns tool_not_found,
      pointing at appduct_list_tools and appduct_call_tool.
    • <alias>__<name> namespacing. With several devices connected, pass selector (the
      session alias or id) instead.
    • notifications/tools/list_changed, and the listChanged capability.
    • MCP-level outputSchema enforcement and schema degradation: schemas reach the agent as
      data through appduct_describe_tool, exactly as registered, whatever their root type. The
      React Native SDK no longer warns about non-object output schemas.
    • MCP client permission rules that named individual app tools (for example
      mcp__appduct__seed_cart) no longer match anything; the client's permission now covers
      appduct_call_tool as a whole, so "always allow" there approves every app tool. To keep a
      person approving destructive calls, set policy.destructive to "prompt" (it covers tools
      annotated destructiveHint: true). "prompt"-policy
      consent itself is unchanged: it is asked per call, via elicitation.
    • The React Native SDK's input-schema warning now fires only for a root type that rules out
      an object (z.string(), z.array(...)), not for unions or intersections of objects.
    • @appduct/shared no longer exports isObjectRootedSchema.
  • Breaking (MCP): "prompt"-policy consent is elicitation-only. The Claude Code-specific
    fallback is gone: tools/list no longer emits _meta["anthropic/requiresUserInteraction"], and
    the MCP server no longer sends consent: "client". A "prompt" tool called from an MCP client
    that doesn't declare the elicitation capability is now denied with policy_denied (reason
    no_consent_channel), the same as the CLI. Current Claude Code declares elicitation, so it gets
    the elicitation prompt instead; only a client that relied on the flag without supporting
    elicitation loses access. To fix that, use a client that supports elicitation, or set the tool's
    policy to "allow" in config.json — which removes the gate for every caller, including the
    CLI — and restart the daemon (appduct daemon stop), since config.json is read once at daemon
    start. The restart disconnects every device, which then has to link again, and a running
    appduct mcp loses its daemon connection, so restart the MCP server in your client too.

    • A daemon with this change that receives consent: "client" from an older MCP server treats it
      as no consent, so the call is denied and audited as no_consent_channel.
    • @appduct/shared: ToolsCallParams.consent and the audit record's consent narrow to
      "elicitation". New audit records never carry "client"; existing audit files may.
  • config.json's wssPort accepts 0, meaning "bind an OS-assigned port". The pinned-wss
    listener takes whatever ephemeral port the OS hands it, and everything that reports or advertises
    the port — daemon.status's wssPort, a minted link's endpoint.port, and so the deep link and
    QR code composed from it — carries the bound port rather than the configured 0. This lets
    several daemons (separate state dirs) coexist on one machine without an operator hand-picking a
    port for each. Every other value must still be a port number in 1..65535; the default is
    unchanged at 8443.

  • Fixed: a zombie daemon process no longer blocks pidfile takeover. process.kill(pid, 0)
    succeeds for an exited-but-unreaped process, so a daemon that was killed after its parent CLI had
    exited could keep its pidfile looking live — in containers whose PID 1 does not reap, for the
    life of the container, leaving every later command reporting a daemon that was already dead. The
    liveness probe now also reads /proc/<pid>/status on Linux and treats State: Z as dead;
    everywhere /proc is absent or unreadable the previous behaviour is unchanged.

  • Breaking (CLI): --json output is compact by default. Every appduct <command> --json
    invocation used to pretty-print its JSON with 2-space indentation; it now prints it on a single
    line (JSON.stringify, no whitespace). Any JSON parser is unaffected. A script that greps or
    diffs the indented text directly is not — pass the new --pretty flag to restore the old
    indentation.

  • Breaking (CLI): the meta block (command, timestamp, duration_ms) is no longer emitted
    by default
    , in either human or --json output. Pass the new --verbose flag to restore it —
    the trailing Meta lines in human mode, the meta field on the --json envelope.

  • New: --pretty and --verbose global flags, alongside --json and --no-color. See the
    [appduct READMEhttps://github.com/callstackincubator/appduct/blob/main/packages/appduct/README.md) for the full description of each.

  • Breaking (CLI): appduct tools --json for a listing now returns { tools, total } instead
    of a bare array. The single-tool form (appduct tools <selector> <name>) is unchanged — it still
    returns the bare tool descriptor.

  • New: a signature-based tools listing, with --filter/--limit/--offset. The human
    listing now shows one call signature (name(params) -> result) plus a one-line description per
    tool instead of a bare name/description table, and appduct tools gains --filter <text> to
    narrow by name/description, and --limit <n>/--offset <n> to page through a large registry —
    making it cheap to read appduct tools against an app that registers hundreds of tools. See the
    [appduct READMEhttps://github.com/callstackincubator/appduct/blob/main/packages/appduct/README.md)'s "appduct tools: a signature per tool" section
    for details.

  • The appduct agent skill now defaults its example commands to plain-text output, adding
    --json only where a script (not the agent itself) will parse the result.

  • Fixed: a bad argument to tools, invoke, revoke or events (a missing <tool>, too
    many positionals, --limit 0) crashed the CLI with a stack trace instead of printing a usage
    error with exit code 64. A numeric flag given without a value (--limit, --limit -1,
    --since, --ttl, --timeout) is now a usage error; it used to be read as 1.

  • Breaking: --open android / target: "android" now require the installed app's id —
    including an Android device appduct_connect auto-detects, which now fails with
    invalid_request until an app id is configured.
    Previously adb shell am start was invoked with an implicit intent (no -p); when more than
    one installed app declared the deep-link scheme, Android showed an "Open with" chooser and
    am start still reported success, so appduct_wait_for_session/the CLI blocked its whole
    timeout with nothing explaining why (issue #63). Delivery now names the package explicitly
    (am start ... -p <app-id>) and requires an app id rather than falling back — for both
    android and the experimental ios-device target.

    • Migration: run appduct init --scheme <s> --android-app-id <id> --ios-app-id <id> in
      your app root once (writes appId.android/appId.ios into .appduct/config.json), or pass
      --app-id <id> on appduct link / appId on the MCP appduct_connect tool / appId on
      mintLink/appduct/client's link() per call. ios-sim needs none of this — it is a usage
      error to pass one there.
    • Removed: --bundle-id (CLI), bundleId (MCP appduct_connect), bundleId
      (mintLink/appduct/client's link()), and config.json's iosBundleId — all replaced by
      --app-id/appId/appId.<platform> above, which now also covers android. There is no
      deprecation shim: a leftover iosBundleId in config.json is silently ignored (an unknown
      key just warns, and loadConfig's warn defaults to a no-op), so the delivery-time error
      above is the only signal that it needs replacing.

Full history: CHANGELOG.md