Module: Ai2Web::Export

Defined in:
lib/ai2web/export.rb

Overview

Export adapters (RFC-0015): project the one canonical AI2Web manifest into other wire formats and discovery surfaces. Port of @ai2web/core export.ts.

Each export is a best-effort projection; where a target cannot represent a field, it is omitted rather than misstated. The canonical /ai2w manifest stays authoritative for execution.

Class Method Summary collapse

Class Method Details

.enabled_capabilities(m) ⇒ Object



12
13
14
15
# File 'lib/ai2web/export.rb', line 12

def enabled_capabilities(m)
  caps = m["capabilities"].is_a?(Hash) ? m["capabilities"] : {}
  caps.select { |_k, v| Util.enabled?(v) }.keys
end

.to_agent_json(manifest) ⇒ Object

Project the manifest to a generic agent.json style capability document. Best-effort, format-neutral projection of identity, capabilities, actions (with bindings), knowledge and policies. Consent/governance a target cannot express are carried as a policies object rather than dropped silently.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/ai2web/export.rb', line 55

def to_agent_json(manifest)
  m = Util.deep_stringify(manifest)
  site = m["site"].is_a?(Hash) ? m["site"] : {}
  actions = m["actions"].is_a?(Array) ? m["actions"] : []
  consent = m["consent"].is_a?(Hash) ? m["consent"] : {}
  {
    "schema" => "agent-capabilities",
    "name" => site["name"],
    "description" => site["description"],
    "url" => site["url"],
    "identity" => m["identity"],
    "capabilities" => enabled_capabilities(m),
    "actions" => actions.map do |a|
      bindings = a["bindings"].is_a?(Array) && !a["bindings"].empty? ? a["bindings"] : [{ "kind" => "rest", "ref" => a["endpoint"] }]
      {
        "name" => a["name"],
        "intent" => a["intent"],
        "description" => a["description"],
        "risk" => a["risk"],
        "requires_consent" => a["requires_user_approval"],
        "requires_auth" => a["requires_auth"],
        "input_schema" => a["input_schema"],
        "bindings" => bindings
      }
    end,
    "knowledge" => m["knowledge"],
    "transports" => m["transports"],
    "policies" => {
      "consent" => consent["requires_user_approval_for"],
      "governance" => m["governance"],
      "usage" => m["usage_policy"],
      "legal" => m["legal"]
    }
  }
end

.to_content_signals(manifest) ⇒ Object

Map usage_policy onto Content Signals tokens. search stays yes because AI2Web exists to be discoverable; the AI signals are only asserted when the manifest states them, so an unset policy is never reported as a refusal. Nil when no policy is declared.



128
129
130
131
132
133
134
135
136
137
# File 'lib/ai2web/export.rb', line 128

def to_content_signals(manifest)
  m = Util.deep_stringify(manifest)
  p = m["usage_policy"]
  return nil unless p.is_a?(Hash) && !p.empty?

  signals = ["search=yes"]
  signals << "ai-input=#{p["content_reproduction"] ? "yes" : "no"}" if [true, false].include?(p["content_reproduction"])
  signals << "ai-train=#{p["model_training"] ? "yes" : "no"}" if [true, false].include?(p["model_training"])
  signals.join(", ")
end

Value for an HTTP Link header advertising the manifest to non-HTML clients.



155
156
157
158
159
# File 'lib/ai2web/export.rb', line 155

def to_discovery_link_header(manifest)
  m = Util.deep_stringify(manifest)
  site = m["site"].is_a?(Hash) ? m["site"] : {}
  "<#{Util.trim_url(site["url"].to_s)}/ai2w>; rel=\"ai2w\""
end

.to_llms_txt(manifest) ⇒ Object

Project the manifest to an llms.txt document: a plain-text summary and set of links a model can read for content and guidance. Reads only; no actions are exposed here.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/ai2web/export.rb', line 19

def to_llms_txt(manifest)
  m = Util.deep_stringify(manifest)
  site = m["site"].is_a?(Hash) ? m["site"] : {}
  base = Util.trim_url(site["url"].to_s)
  lines = ["# #{site["name"]}"]
  lines += ["", "> #{site["description"]}"] if Util.truthy?(site["description"])

  caps = enabled_capabilities(m)
  lines += ["", "## Capabilities", *caps.map { |c| "- #{c}" }] unless caps.empty?

  knowledge = m["knowledge"].is_a?(Array) ? m["knowledge"] : []
  unless knowledge.empty?
    lines << ""
    lines << "## Knowledge"
    knowledge.each do |k|
      ref = k["ref"].to_s
      ref = base + (ref.start_with?("/") ? "" : "/") + ref unless ref.start_with?("http")
      lines << "- [#{k["name"] || k["id"]}](#{ref})"
    end
  end

  actions = m["actions"].is_a?(Array) ? m["actions"] : []
  unless actions.empty?
    lines << ""
    lines << "## Actions"
    actions.each { |a| lines << "- #{a["name"]}: #{a["description"]}" }
  end

  lines += ["", "## Discovery", "- Manifest: #{base}/ai2w"]
  "#{lines.join("\n")}\n"
end

.to_oauth_protected_resource(manifest) ⇒ Object

OAuth 2.0 Protected Resource metadata (RFC 9728), for /.well-known/oauth-protected-resource. MCP clients read this to discover which authorization server guards the resource before starting a flow.

Returns nil when the site does not advertise oauth2, so an auth surface the site cannot honour is never published.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/ai2web/export.rb', line 97

def to_oauth_protected_resource(manifest)
  m = Util.deep_stringify(manifest)
  auth = m["auth"].is_a?(Hash) ? m["auth"] : {}
  return nil unless Array(auth["methods"]).include?("oauth2")

  site = m["site"].is_a?(Hash) ? m["site"] : {}
  base = Util.trim_url(site["url"].to_s)
  oauth2 = auth["oauth2"].is_a?(Hash) ? auth["oauth2"] : {}
  issuer = base
  authz = oauth2["authorization_url"].to_s
  unless authz.empty?
    begin
      u = URI.parse(authz)
      issuer = "#{u.scheme}://#{u.host}#{u.port && ![80, 443].include?(u.port) ? ":#{u.port}" : ""}" if u.scheme && u.host
    rescue URI::InvalidURIError
      # keep the site base as issuer
    end
  end
  doc = {
    "resource" => "#{base}/ai2w",
    "authorization_servers" => [issuer],
    "bearer_methods_supported" => ["header"]
  }
  scopes = oauth2["scopes"]
  doc["scopes_supported"] = Array(scopes) if scopes && !Array(scopes).empty?
  doc
end

.to_robots_txt(manifest) ⇒ Object

A robots.txt FRAGMENT carrying the usage policy and a pointer to the manifest. Append it to an existing robots.txt; it is never a replacement, and emits no Disallow rules.



141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/ai2web/export.rb', line 141

def to_robots_txt(manifest)
  m = Util.deep_stringify(manifest)
  site = m["site"].is_a?(Hash) ? m["site"] : {}
  base = Util.trim_url(site["url"].to_s)
  lines = ["# AI2Web usage policy, projected from #{base}/ai2w", "User-agent: *"]
  signals = to_content_signals(m)
  lines << "Content-Signal: #{signals}" unless signals.nil?
  lines << "# bulk_extraction: false - please use the /ai2w endpoints instead of crawling" if
    m["usage_policy"].is_a?(Hash) && m["usage_policy"]["bulk_extraction"] == false
  lines << "# AI2Web-Manifest: #{base}/ai2w"
  "#{lines.join("\n")}\n"
end